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 }