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 "afcclient.h" 0007 0008 #include "afc_debug.h" 0009 0010 #include "afcdevice.h" 0011 #include "afcfile.h" 0012 #include "afcutils.h" 0013 0014 #include <QDateTime> 0015 #include <QScopeGuard> 0016 0017 using namespace KIO; 0018 0019 AfcClient::AfcClient(AfcDevice *device) 0020 : m_device(device) 0021 { 0022 } 0023 0024 AfcClient::~AfcClient() 0025 { 0026 if (m_client) { 0027 afc_client_free(m_client); 0028 m_client = nullptr; 0029 } 0030 0031 if (m_houseArrestClient) { 0032 house_arrest_client_free(m_houseArrestClient); 0033 m_houseArrestClient = nullptr; 0034 } 0035 } 0036 0037 WorkerResult AfcClient::init(lockdownd_client_t lockdowndClient, const QString &appId) 0038 { 0039 if (m_client) { 0040 return WorkerResult::pass(); 0041 } 0042 0043 const char *serviceName = appId.isEmpty() ? AFC_SERVICE_NAME : HOUSE_ARREST_SERVICE_NAME; 0044 0045 lockdownd_service_descriptor_t service = nullptr; 0046 auto ret = lockdownd_start_service(lockdowndClient, serviceName, &service); 0047 if (ret != LOCKDOWN_E_SUCCESS) { 0048 qCWarning(KIO_AFC_LOG) << "Failed to start" << serviceName << "service through lockdownd" << ret; 0049 return AfcUtils::Result::from(ret, m_device->id()); 0050 } 0051 0052 // Regular file system access through AFC client 0053 if (appId.isEmpty()) { 0054 auto afcRet = afc_client_new(m_device->device(), service, &m_client); 0055 if (afcRet != AFC_E_SUCCESS) { 0056 qCWarning(KIO_AFC_LOG) << "Failed to create AFC client" << afcRet; 0057 return AfcUtils::Result::from(ret, m_device->id()); 0058 } 0059 m_appId = appId; 0060 return WorkerResult::pass(); 0061 } 0062 0063 // App-specific document folder through specialized House Arrest AFC client 0064 if (!m_houseArrestClient) { 0065 auto houseArrestRet = house_arrest_client_new(m_device->device(), service, &m_houseArrestClient); 0066 if (houseArrestRet != HOUSE_ARREST_E_SUCCESS) { 0067 qCWarning(KIO_AFC_LOG) << "Failed to create House Arrest client" << houseArrestRet; 0068 return AfcUtils::Result::from(houseArrestRet); 0069 } 0070 0071 auto cleanupHouseArrest = qScopeGuard([this] { 0072 house_arrest_client_free(m_houseArrestClient); 0073 m_houseArrestClient = nullptr; 0074 }); 0075 0076 const char houseArrestCommand[] = "VendDocuments"; 0077 houseArrestRet = house_arrest_send_command(m_houseArrestClient, houseArrestCommand, appId.toUtf8().constData()); 0078 if (houseArrestRet != HOUSE_ARREST_E_SUCCESS) { 0079 qCWarning(KIO_AFC_LOG) << "Failed to send House Arrest" << houseArrestCommand << "command" << houseArrestRet; 0080 return AfcUtils::Result::from(houseArrestRet); 0081 } 0082 0083 plist_t resultPlist = nullptr; 0084 houseArrestRet = house_arrest_get_result(m_houseArrestClient, &resultPlist); 0085 if (houseArrestRet != HOUSE_ARREST_E_SUCCESS) { 0086 qCWarning(KIO_AFC_LOG) << "Failed to get House Arrest result for" << houseArrestCommand << houseArrestRet; 0087 return AfcUtils::Result::from(houseArrestRet); 0088 } 0089 0090 auto cleanupPlist = qScopeGuard([resultPlist] { 0091 plist_free(resultPlist); 0092 }); 0093 0094 plist_t errorItem = plist_dict_get_item(resultPlist, "Error"); 0095 if (errorItem) { 0096 char *errorString = nullptr; 0097 plist_get_string_val(errorItem, &errorString); 0098 QScopedPointer<char, QScopedPointerPodDeleter> errorStringPtr(errorString); 0099 0100 // The app does not exist. 0101 if (strcmp(errorStringPtr.data(), "ApplicationLookupFailed") == 0) { 0102 return WorkerResult::fail(ERR_DOES_NOT_EXIST, appId); 0103 // The app does not have UIFileSharingEnabled enabled. 0104 } else if (strcmp(errorStringPtr.data(), "InstallationLookupFailed") == 0) { 0105 return WorkerResult::fail(ERR_ACCESS_DENIED, appId); 0106 } 0107 0108 qCWarning(KIO_AFC_LOG) << "House Arrest returned error" << errorString; 0109 return WorkerResult::fail(ERR_INTERNAL, QString::fromUtf8(errorString)); 0110 } 0111 0112 cleanupHouseArrest.dismiss(); 0113 } 0114 0115 auto afcRet = afc_client_new_from_house_arrest_client(m_houseArrestClient, &m_client); 0116 if (afcRet != AFC_E_SUCCESS) { 0117 qCWarning(KIO_AFC_LOG) << "Failed to create AFC client from House Arrest client" << afcRet; 0118 return AfcUtils::Result::from(afcRet); 0119 } 0120 0121 m_appId = appId; 0122 return WorkerResult::pass(); 0123 } 0124 0125 AfcDevice *AfcClient::device() const 0126 { 0127 return m_device; 0128 } 0129 0130 afc_client_t AfcClient::internalClient() const 0131 { 0132 return m_client; 0133 } 0134 0135 QString AfcClient::appId() const 0136 { 0137 return m_appId; 0138 } 0139 0140 WorkerResult AfcClient::entry(const QString &path, UDSEntry &entry) 0141 { 0142 char **info = nullptr; 0143 const auto ret = afc_get_file_info(m_client, path.toUtf8().constData(), &info); 0144 // may return null https://github.com/libimobiledevice/libimobiledevice/issues/206 0145 const WorkerResult result = AfcUtils::Result::from(ret, path); 0146 if (!result.success() || !info) { 0147 return result; 0148 } 0149 0150 auto cleanup = qScopeGuard([info] { 0151 afc_dictionary_free(info); 0152 }); 0153 0154 const int lastSlashIdx = path.lastIndexOf(QLatin1Char('/')); 0155 entry.fastInsert(UDSEntry::UDS_NAME, path.mid(lastSlashIdx + 1)); 0156 0157 // Apply special icons for known locations 0158 if (m_appId.isEmpty()) { 0159 static const QHash<QString, QString> s_folderIcons = {{QStringLiteral("/DCIM"), QStringLiteral("camera-photo")}, 0160 {QStringLiteral("/Documents"), QStringLiteral("folder-documents")}, 0161 {QStringLiteral("/Downloads"), QStringLiteral("folder-downloads")}, 0162 {QStringLiteral("/Photos"), QStringLiteral("folder-pictures")}}; 0163 const QString iconName = s_folderIcons.value(path); 0164 if (!iconName.isEmpty()) { 0165 entry.fastInsert(UDSEntry::UDS_ICON_NAME, iconName); 0166 } 0167 } 0168 0169 for (int i = 0; info[i]; i += 2) { 0170 const auto *key = info[i]; 0171 const auto *value = info[i + 1]; 0172 0173 if (strcmp(key, "st_size") == 0) { 0174 entry.fastInsert(UDSEntry::UDS_SIZE, atoll(value)); 0175 } else if (strcmp(key, "st_blocks") == 0) { 0176 // no UDS equivalent 0177 } else if (strcmp(key, "st_nlink") == 0) { 0178 // no UDS equivalent 0179 } else if (strcmp(key, "st_ifmt") == 0) { 0180 int type = 0; 0181 if (strcmp(value, "S_IFREG") == 0) { 0182 type = S_IFREG; 0183 } else if (strcmp(value, "S_IFDIR") == 0) { 0184 type = S_IFDIR; 0185 entry.fastInsert(UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); 0186 } else if (strcmp(value, "S_IFLNK") == 0) { 0187 type = S_IFLNK; 0188 } else if (strcmp(value, "S_IFBLK") == 0) { 0189 type = S_IFBLK; 0190 } else if (strcmp(value, "S_IFCHR") == 0) { 0191 type = S_IFCHR; 0192 } else if (strcmp(value, "S_IFIFO") == 0) { 0193 type = S_IFIFO; 0194 } else if (strcmp(value, "S_IFSOCK") == 0) { 0195 type = S_IFSOCK; 0196 } 0197 0198 if (type) { 0199 entry.fastInsert(UDSEntry::UDS_FILE_TYPE, type); 0200 } else { 0201 qCWarning(KIO_AFC_LOG) << "Encountered unknown" << key << "of" << value << "for" << path; 0202 } 0203 // is returned in nanoseconds 0204 } else if (strcmp(key, "st_mtime") == 0) { 0205 entry.fastInsert(UDSEntry::UDS_MODIFICATION_TIME, atoll(value) / 1000000000); 0206 } else if (strcmp(key, "st_birthtime") == 0) { 0207 entry.fastInsert(UDSEntry::UDS_CREATION_TIME, atoll(value) / 1000000000); 0208 } else if (strcmp(key, "LinkTarget") == 0) { 0209 // TODO figure out why afc_make_link fails with AFC_E_OP_NOT_SUPPORTED and then implement this 0210 // entry.fastInsert(UDSEntry::UDS_LINK_DEST, QString::fromUtf8(value)); 0211 } else { 0212 qCDebug(KIO_AFC_LOG) << "Encountered unrecognized file info key" << key << "for" << path; 0213 } 0214 } 0215 0216 return WorkerResult::pass(); 0217 } 0218 0219 WorkerResult AfcClient::entryList(const QString &path, QStringList &entryList) 0220 { 0221 char **entries = nullptr; 0222 entryList.clear(); 0223 0224 const auto ret = afc_read_directory(m_client, path.toUtf8().constData(), &entries); 0225 const WorkerResult result = AfcUtils::Result::from(ret); 0226 if (!result.success() || !entries) { 0227 return result; 0228 } 0229 0230 auto cleanup = qScopeGuard([entries] { 0231 afc_dictionary_free(entries); 0232 }); 0233 0234 char **it = entries; 0235 while (*it) { 0236 const QString name = QString::fromUtf8(*it); 0237 ++it; 0238 0239 if (name == QLatin1Char('.') || name == QLatin1String("..")) { 0240 continue; 0241 } 0242 entryList.append(name); 0243 } 0244 0245 return WorkerResult::pass(); 0246 } 0247 0248 WorkerResult AfcClient::del(const QString &path) 0249 { 0250 const auto ret = afc_remove_path(m_client, path.toUtf8().constData()); 0251 return AfcUtils::Result::from(ret, path); 0252 } 0253 0254 WorkerResult AfcClient::delRecursively(const QString &path) 0255 { 0256 const auto ret = afc_remove_path_and_contents(m_client, path.toUtf8().constData()); 0257 return AfcUtils::Result::from(ret, path); 0258 } 0259 0260 WorkerResult AfcClient::rename(const QString &src, const QString &dest, JobFlags flags) 0261 { 0262 UDSEntry srcEntry; // unused 0263 const WorkerResult srcResult = this->entry(src, srcEntry); 0264 if (!srcResult.success()) { 0265 return srcResult; 0266 } 0267 0268 UDSEntry destEntry; 0269 const auto destResult = this->entry(dest, destEntry); 0270 0271 const bool exists = destResult.error() != ERR_DOES_NOT_EXIST; 0272 if (exists && !flags.testFlag(Overwrite)) { 0273 if (S_ISDIR(destEntry.numberValue(UDSEntry::UDS_FILE_TYPE))) { 0274 return WorkerResult::fail(ERR_DIR_ALREADY_EXIST, dest); 0275 } 0276 return WorkerResult::fail(ERR_FILE_ALREADY_EXIST, dest); 0277 } 0278 0279 const auto ret = afc_rename_path(m_client, src.toUtf8().constData(), dest.toUtf8().constData()); 0280 return AfcUtils::Result::from(ret, dest); 0281 } 0282 0283 WorkerResult AfcClient::symlink(const QString &target, const QString &dest, JobFlags flags) 0284 { 0285 UDSEntry targetEntry; // unused 0286 const WorkerResult targetResult = this->entry(target, targetEntry); 0287 if (!targetResult.success()) { 0288 return targetResult; 0289 } 0290 0291 UDSEntry destEntry; 0292 const auto destResult = this->entry(dest, destEntry); 0293 0294 const bool exists = destResult.error() != ERR_DOES_NOT_EXIST; 0295 if (exists && !flags.testFlag(Overwrite)) { 0296 if (S_ISDIR(destEntry.numberValue(UDSEntry::UDS_FILE_TYPE))) { 0297 return WorkerResult::fail(ERR_DIR_ALREADY_EXIST, dest); 0298 } 0299 return WorkerResult::fail(ERR_FILE_ALREADY_EXIST, dest); 0300 } 0301 0302 // TODO figure out why this always fails with AFC_E_OP_NOT_SUPPORTED 0303 const auto ret = afc_make_link(m_client, AFC_SYMLINK, target.toUtf8().constData(), dest.toUtf8().constData()); 0304 return AfcUtils::Result::from(ret, dest); 0305 } 0306 0307 WorkerResult AfcClient::mkdir(const QString &path) 0308 { 0309 UDSEntry entry; 0310 const WorkerResult getResult = this->entry(path, entry); 0311 0312 const bool exists = getResult.error() != ERR_DOES_NOT_EXIST; 0313 if (exists) { 0314 if (S_ISDIR(entry.numberValue(UDSEntry::UDS_FILE_TYPE))) { 0315 return WorkerResult::fail(ERR_DIR_ALREADY_EXIST, path); 0316 } 0317 return WorkerResult::fail(ERR_FILE_ALREADY_EXIST, path); 0318 } 0319 0320 const auto ret = afc_make_directory(m_client, path.toUtf8().constData()); 0321 return AfcUtils::Result::from(ret, path); 0322 } 0323 0324 WorkerResult AfcClient::setModificationTime(const QString &path, const QDateTime &mtime) 0325 { 0326 const auto ret = afc_set_file_time(m_client, path.toUtf8().constData(), mtime.toMSecsSinceEpoch() /*ms*/ * 1000000 /*us*/); 0327 return AfcUtils::Result::from(ret, path); 0328 }