From 56fa9310a60245a7ea355018ee2f53a3b3124c45 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Sat, 4 Apr 2026 10:32:55 +0000 Subject: [PATCH] fix: use hause_arrest or afc2 client whenever possible --- src/afcexplorerwidget.cpp | 117 +++-- src/afcexplorerwidget.h | 7 +- src/appcontext.cpp | 15 +- src/appdownloaddialog.cpp | 2 +- src/fileexplorerwidget.cpp | 18 +- src/iDescriptor.h | 2 + src/imageloader.cpp | 31 +- src/imageloader.h | 12 +- src/imagetask.h | 15 +- src/installedappswidget.cpp | 6 +- src/iomanagerclient.cpp | 94 +++- src/iomanagerclient.h | 12 +- src/rust/build.rs | 1 + src/rust/src/afc2_services.rs | 781 ++++++++++++++++++++++++++++++++++ src/rust/src/afc_services.rs | 262 +++++++----- src/rust/src/hause_arrest.rs | 9 + src/rust/src/io_manager.rs | 21 +- src/rust/src/lib.rs | 241 +++++------ src/rust/src/utils.rs | 15 +- 19 files changed, 1315 insertions(+), 346 deletions(-) create mode 100644 src/rust/src/afc2_services.rs diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp index 1f3597e..8ce1b2b 100644 --- a/src/afcexplorerwidget.cpp +++ b/src/afcexplorerwidget.cpp @@ -45,11 +45,11 @@ AfcExplorerWidget::AfcExplorerWidget( const std::shared_ptr device, bool favEnabled, - std::optional> hause_arrest, QString root, - QWidget *parent) + std::optional> hause_arrest, bool useAfc2, + QString root, QWidget *parent) : QWidget(parent), m_device(device), m_favEnabled(favEnabled), m_hauseArrest(hause_arrest), m_errorMessage("Failed to load directory"), - m_root(root) + m_root(root), m_useAfc2(useAfc2) { QVBoxLayout *rootLayout = new QVBoxLayout(this); @@ -74,7 +74,26 @@ AfcExplorerWidget::AfcExplorerWidget( rootLayout->addWidget(m_loadingWidget); m_loadingWidget->setupContentWidget(contentContainer); - if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { + connect(m_loadingWidget, &ZLoadingWidget::retryClicked, this, [this]() { + m_loadingWidget->showLoading(); + QTimer::singleShot(100, this, [this]() { loadPath(m_history.top()); }); + }); + + if (m_useAfc2) { + bool is_available = m_device->afc2_backend->is_available(); + if (!is_available) { + qDebug() + << "[AfcExplorerWidget] AFC2 is not available on this device."; + m_loadingWidget->showError("AFC2 is not available on this device."); + return; + } + } + + if (m_useAfc2) { + connect(m_device->afc2_backend, + &CXX::Afc2Backend::check_is_dir_and_list_finished, this, + &AfcExplorerWidget::onLoadPathFinished); + } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { connect(m_hauseArrest.value().get(), &CXX::HauseArrest::check_is_dir_and_list_finished, this, &AfcExplorerWidget::onLoadPathFinished); @@ -203,11 +222,26 @@ void AfcExplorerWidget::updateAddressBar(const QString &path) void AfcExplorerWidget::loadPath(const QString &path) { m_loadingWidget->showLoading(); + + if (m_useAfc2) { + bool is_available = m_device->afc2_backend->is_available(); + if (!is_available) { + qDebug() + << "[AfcExplorerWidget] AFC2 is not available on this device."; + m_loadingWidget->showError("AFC2 is not available on this device."); + return; + } + } + updateAddressBar(path); updateNavigationButtons(); + // FIXME: we need a better approach to this + // similar code is repeated in some places /* use the correct afc client */ - if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { + if (m_useAfc2) { + m_device->afc2_backend->check_is_dir_and_list(path); + } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { m_hauseArrest.value()->check_is_dir_and_list(path); } else { m_device->afc_backend->check_is_dir_and_list(path); @@ -286,31 +320,8 @@ void AfcExplorerWidget::onFileListContextMenu(const QPoint &pos) if (filesToExport.isEmpty()) return; - QString dir = - QFileDialog::getExistingDirectory(this, "Select Export Directory"); - if (dir.isEmpty()) - return; + handleExport(filesToExport); - // FIXME - // Convert to ExportItem list - // QList exportItems; - // QString currPath = "/"; - // if (!m_history.isEmpty()) - // currPath = m_history.top(); - // if (!currPath.endsWith("/")) - // currPath += "/"; - - // for (QListWidgetItem *selItem : filesToExport) { - // QString fileName = selItem->text(); - // QString devicePath = - // currPath == "/" ? "/" + fileName : currPath + fileName; - // exportItems.append( - // ExportItem(devicePath, fileName, m_device->udid)); - // } - - // ExportManager::sharedInstance()->startExport( - // m_device, exportItems, dir, "Exporting from file Explorer", - // m_afc); } else if (selectedAction == openAction) { onItemDoubleClicked(item); } else if (selectedAction == openNativeAction) { @@ -333,31 +344,45 @@ void AfcExplorerWidget::onExportClicked() if (filesToExport.isEmpty()) return; + handleExport(filesToExport); +} + +void AfcExplorerWidget::handleExport(QList filesToExport) +{ + // Ask user for a directory to save all files QString dir = QFileDialog::getExistingDirectory(this, "Select Export Directory"); if (dir.isEmpty()) return; - // FIXME - // // Convert to ExportItem list - // QList exportItems; - // QString currPath = "/"; - // if (!m_history.isEmpty()) - // currPath = m_history.top(); - // if (!currPath.endsWith("/")) - // currPath += "/"; + QList exportItems; + QString currPath = "/"; + if (!m_history.isEmpty()) + currPath = m_history.top(); + if (!currPath.endsWith("/")) + currPath += "/"; - // for (QListWidgetItem *item : filesToExport) { - // QString fileName = item->text(); - // QString devicePath = - // currPath == "/" ? "/" + fileName : currPath + fileName; - // exportItems.append(ExportItem(devicePath, fileName, m_device->udid)); - // } + for (QListWidgetItem *selItem : filesToExport) { + QString fileName = selItem->text(); + QString devicePath = + currPath == "/" ? "/" + fileName : currPath + fileName; + exportItems.append(devicePath); + } - // // Start export - // ExportManager::sharedInstance()->startExport( - // m_device, exportItems, dir, "Exporting from file Explorer", m_afc); + if (m_useAfc2) { + IOManagerClient::sharedInstance()->startExport( + m_device, exportItems, dir, "Exporting from File Explorer ", + true); + } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { + IOManagerClient::sharedInstance()->startExport( + m_device, exportItems, dir, + "Exporting from File Explorer (App Container)", + m_hauseArrest.value()->get_bundle_id()); + } else { + IOManagerClient::sharedInstance()->startExport( + m_device, exportItems, dir, "Exporting from File Explorer", true); + } } void AfcExplorerWidget::exportAndOpenSelectedFile(QListWidgetItem *item, diff --git a/src/afcexplorerwidget.h b/src/afcexplorerwidget.h index 849bb0f..01fbb0d 100644 --- a/src/afcexplorerwidget.h +++ b/src/afcexplorerwidget.h @@ -22,6 +22,7 @@ #include "iDescriptor-ui.h" #include "iDescriptor.h" +#include "iomanagerclient.h" #include "zloadingwidget.h" #include #include @@ -50,7 +51,8 @@ public: bool favEnabled = false, std::optional> hause_arrest = std::nullopt, - QString root = "/", QWidget *parent = nullptr); + bool useAfc2 = false, QString root = "/", + QWidget *parent = nullptr); void navigateToPath(const QString &path); void goHome(); signals: @@ -95,6 +97,7 @@ private: QString m_errorMessage; QString m_root; ZLoadingWidget *m_loadingWidget; + bool m_useAfc2; // Export system ExportManager *m_exportManager; @@ -119,6 +122,8 @@ private: void onLoadPathFinished(bool success, const QMap &entries); + void handleExport(QList filesToExport); + #ifndef WIN32 void updateNavStyles(); diff --git a/src/appcontext.cpp b/src/appcontext.cpp index afe3b2c..f726501 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -61,6 +61,7 @@ void AppContext::cachePairedDevices() #endif } +/* addDevice is only called with udid from backend */ void AppContext::addDevice(iDescriptor::Uniq uniq, iDescriptor::IdeviceConnectionType conn_type, AddType addType, QString info, @@ -73,10 +74,7 @@ void AppContext::addDevice(iDescriptor::Uniq uniq, } std::shared_ptr existingDevice = nullptr; - // existingDevice = getDeviceByMacAddress(uniq.get()); - if (!existingDevice) { - existingDevice = getDevice(uniq.get()); - } + existingDevice = getDevice(uniq.get()); if (existingDevice) { uniq.isMac() ? emit deviceAlreadyExistsMAC(uniq) @@ -88,7 +86,7 @@ void AppContext::addDevice(iDescriptor::Uniq uniq, } if (addType == AddType::Pairing) { - // handlePairing(uniq, true); + handlePairing(uniq, true); return; } @@ -130,7 +128,8 @@ void AppContext::addDevice(iDescriptor::Uniq uniq, .ios_version = deviceInfo.parsedDeviceVersion.major, .service_manager = new CXX::ServiceManager( uniq.get(), deviceInfo.parsedDeviceVersion.major), - .afc_backend = new CXX::AfcBackend(uniq.get())}; + .afc_backend = new CXX::AfcBackend(uniq.get()), + .afc2_backend = new CXX::Afc2Backend(uniq.get())}; m_devices[device.udid] = std::make_shared(device); @@ -284,10 +283,6 @@ void AppContext::addRecoveryDevice(uint64_t ecid) AppContext::~AppContext() { - for (auto device : m_devices) { - // freeDevice(device); - } - m_devices.clear(); #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT diff --git a/src/appdownloaddialog.cpp b/src/appdownloaddialog.cpp index 20ce649..7330579 100644 --- a/src/appdownloaddialog.cpp +++ b/src/appdownloaddialog.cpp @@ -45,7 +45,7 @@ AppDownloadDialog::AppDownloadDialog(const QString &appName, setFixedWidth(500); setContentsMargins(0, 0, 0, 0); - m_loadingWidget = new ZLoadingWidget(true, this); + m_loadingWidget = new ZLoadingWidget(false, this); layout()->addWidget(m_loadingWidget); QVBoxLayout *contentLayout = new QVBoxLayout(); contentLayout->setContentsMargins(0, 0, 0, 0); diff --git a/src/fileexplorerwidget.cpp b/src/fileexplorerwidget.cpp index 2a9588f..2e87c20 100644 --- a/src/fileexplorerwidget.cpp +++ b/src/fileexplorerwidget.cpp @@ -31,7 +31,7 @@ FileExplorerWidget::FileExplorerWidget( QVBoxLayout *rootLayout = new QVBoxLayout(this); rootLayout->setContentsMargins(0, 0, 0, 0); - m_loadingWidget = new ZLoadingWidget(true, this); + m_loadingWidget = new ZLoadingWidget(false, this); rootLayout->addWidget(m_loadingWidget); } @@ -60,20 +60,18 @@ void FileExplorerWidget::init() // Add normal AFC explorer (index 0) AfcExplorerWidget *afcExplorer = - new AfcExplorerWidget(m_device, true, std::nullopt, "/", this); + new AfcExplorerWidget(m_device, true, std::nullopt, false, "/", this); connect(afcExplorer, &AfcExplorerWidget::favoritePlaceAdded, this, &FileExplorerWidget::saveFavoritePlace); m_stackedWidget->addWidget(afcExplorer); - // FIXME: AFC2 - // // Add AFC2 explorer (index 1) - // AfcExplorerWidget *afc2Explorer = - // new AfcExplorerWidget(m_device, true, m_device->afc2Client, "/", - // this); - // connect(afc2Explorer, &AfcExplorerWidget::favoritePlaceAdded, this, - // &FileExplorerWidget::saveFavoritePlaceAfc2); - // m_stackedWidget->addWidget(afc2Explorer); + // Add AFC2 explorer (index 1) + AfcExplorerWidget *afc2Explorer = + new AfcExplorerWidget(m_device, true, std::nullopt, true, "/", this); + connect(afc2Explorer, &AfcExplorerWidget::favoritePlaceAdded, this, + &FileExplorerWidget::saveFavoritePlaceAfc2); + m_stackedWidget->addWidget(afc2Explorer); // Start with normal AFC client m_stackedWidget->setCurrentIndex(0); diff --git a/src/iDescriptor.h b/src/iDescriptor.h index e232d48..1385290 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -78,6 +78,7 @@ #endif // rust codebase +#include "idescriptor_rust_codebase/src/afc2_services.cxxqt.h" #include "idescriptor_rust_codebase/src/afc_services.cxxqt.h" #include "idescriptor_rust_codebase/src/hause_arrest.cxxqt.h" #include "idescriptor_rust_codebase/src/io_manager.cxxqt.h" @@ -226,6 +227,7 @@ struct iDescriptorDevice { unsigned int ios_version; CXX::ServiceManager *service_manager; CXX::AfcBackend *afc_backend; + CXX::Afc2Backend *afc2_backend; }; void fullDeviceInfo(const pugi::xml_document &doc, DeviceInfo &d); diff --git a/src/imageloader.cpp b/src/imageloader.cpp index fb0f973..99a8b0c 100644 --- a/src/imageloader.cpp +++ b/src/imageloader.cpp @@ -63,14 +63,15 @@ void ImageLoader::requestThumbnail( void ImageLoader::requestImageWithCallback( const std::shared_ptr device, const QString &path, int priority, std::function callback, - std::optional> hause_arrest) + std::optional> hause_arrest, bool useAfc2) { /* FIXME: row is passed as priority nothing dangerous but a bit hacky, could be handled better */ //scale=false - auto *task = new ImageTask(device, path, priority, false, hause_arrest); + auto *task = + new ImageTask(device, path, priority, false, hause_arrest, useAfc2); /* TODO: should we do this ? @@ -174,7 +175,7 @@ void ImageLoader::onTaskFinished(const QString &path, const QPixmap &pixmap, // almost a copy of loadThumbnailFromDevice but without any scaling logic QPixmap ImageLoader::loadImage( const std::shared_ptr device, const QString &filePath, - std::optional> hause_arrest) + std::optional> hause_arrest, bool useAfc2) { if (QCoreApplication::closingDown()) { qDebug() << "Application is closing, aborting loadImage for" @@ -183,7 +184,9 @@ QPixmap ImageLoader::loadImage( } QByteArray imageData; - if (hause_arrest.has_value() && hause_arrest.value()) { + if (useAfc2) { + imageData = device->afc2_backend->file_to_buffer(filePath); + } else if (hause_arrest.has_value() && hause_arrest.value()) { qDebug() << "Loading image using HauseArrest for:" << filePath; imageData = hause_arrest.value()->file_to_buffer(filePath); } else { @@ -226,7 +229,7 @@ QPixmap ImageLoader::loadImage( QPixmap ImageLoader::loadThumbnailFromDevice( const std::shared_ptr device, const QString &filePath, const QSize &size, - std::optional> hause_arrest) + std::optional> hause_arrest, bool useAfc2) { if (QCoreApplication::closingDown()) { qDebug() @@ -235,7 +238,16 @@ QPixmap ImageLoader::loadThumbnailFromDevice( return {}; } - QByteArray imageData = device->afc_backend->file_to_buffer(filePath); + QByteArray imageData; + + if (useAfc2) { + device->afc2_backend->file_to_buffer(filePath); + } else if (hause_arrest.has_value() && hause_arrest.value()) { + qDebug() << "Loading thumbnail using HauseArrest for:" << filePath; + imageData = hause_arrest.value()->file_to_buffer(filePath); + } else { + imageData = device->afc_backend->file_to_buffer(filePath); + } if (imageData.isEmpty()) { qDebug() << "Could not read from device:" << filePath; @@ -277,7 +289,7 @@ QPixmap ImageLoader::loadThumbnailFromDevice( QPixmap ImageLoader::generateVideoThumbnailFFmpeg( const std::shared_ptr device, const QString &filePath, const QSize &requestedSize, - std::optional> hause_arrest) + std::optional> hause_arrest, bool useAfc2) { QPixmap thumbnail; if (QCoreApplication::closingDown()) { @@ -287,6 +299,11 @@ QPixmap ImageLoader::generateVideoThumbnailFFmpeg( return thumbnail; } + /* + FIXME: other afc clients are not respected here, we need to handle this + better, currently only the normal afc client is used for video thumbnail + generation + */ CXX::AfcBackend *afc = device->afc_backend; const qint64 fileSize = afc->get_file_size(filePath); diff --git a/src/imageloader.h b/src/imageloader.h index 4c66d25..788f7f0 100644 --- a/src/imageloader.h +++ b/src/imageloader.h @@ -31,7 +31,8 @@ public: const std::shared_ptr device, const QString &path, int priority, std::function callback, std::optional> hause_arrest = - std::nullopt); + std::nullopt, + bool useAfc2 = false); void cancelThumbnail(const QString &path); bool isLoading(const QString &path); void clear(); @@ -40,16 +41,19 @@ public: const std::shared_ptr device, const QString &filePath, const QSize &size, std::optional> hause_arrest = - std::nullopt); + std::nullopt, + bool useAfc2 = false); static QPixmap generateVideoThumbnailFFmpeg( const std::shared_ptr device, const QString &filePath, const QSize &size, std::optional> hause_arrest = - std::nullopt); + std::nullopt, + bool useAfc2 = false); static QPixmap loadImage(const std::shared_ptr device, const QString &filePath, std::optional> - hause_arrest = std::nullopt); + hause_arrest = std::nullopt, + bool useAfc2 = false); signals: void thumbnailReady(const QString &path, const QPixmap &image, unsigned int row); diff --git a/src/imagetask.h b/src/imagetask.h index 27939c1..afe1c53 100644 --- a/src/imagetask.h +++ b/src/imagetask.h @@ -18,9 +18,10 @@ public: ImageTask(const std::shared_ptr device, const QString &path, unsigned int row, bool scale = true, std::optional> hause_arrest = - std::nullopt) + std::nullopt, + bool useAfc2 = false) : m_device(device), m_path(path), m_isThumbnail(scale), m_row(row), - m_hause_arrest(hause_arrest) + m_hause_arrest(hause_arrest), m_useAfc2(useAfc2) { setAutoDelete(true); } @@ -35,18 +36,19 @@ protected: if (isVideo) { QPixmap thumbnail = ImageLoader::generateVideoThumbnailFFmpeg( - m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest); + m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest, m_useAfc2); emit finished(m_path, thumbnail, m_row); } else { if (m_isThumbnail) { QPixmap image = ImageLoader::loadThumbnailFromDevice( - m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest); + m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest, + m_useAfc2); emit finished(m_path, image, m_row); } else { qDebug() << "Loading full image for:" << m_path; - QPixmap image = - ImageLoader::loadImage(m_device, m_path, m_hause_arrest); + QPixmap image = ImageLoader::loadImage( + m_device, m_path, m_hause_arrest, m_useAfc2); emit finished(m_path, image, m_row); } } @@ -58,6 +60,7 @@ private: bool m_isThumbnail; unsigned int m_row; std::optional> m_hause_arrest; + bool m_useAfc2; }; #endif // IMAGETASK_H diff --git a/src/installedappswidget.cpp b/src/installedappswidget.cpp index 0e12d4d..177730a 100644 --- a/src/installedappswidget.cpp +++ b/src/installedappswidget.cpp @@ -205,6 +205,7 @@ void InstalledAppsWidget::showLoadingState() void InstalledAppsWidget::showErrorState(const QString &error) { + m_zloadingWidget->stop(true); m_errorLabel->setText(QString("Error loading apps: %1").arg(error)); m_stackedWidget->setCurrentWidget(m_errorWidget); } @@ -285,14 +286,13 @@ void InstalledAppsWidget::createContentWidget() void InstalledAppsWidget::onAppsDataReady(const QMap &result) { - + m_zloadingWidget->stop(true); if (result.isEmpty()) { showErrorState("No apps found or failed to retrieve apps."); return; } m_stackedWidget->setCurrentWidget(m_contentWidget); - m_zloadingWidget->stop(true); // Clear existing tabs qDeleteAll(m_appTabs); @@ -493,7 +493,7 @@ void InstalledAppsWidget::onContainerDataReady(bool success) // Create AfcExplorerWidget with the house arrest AFC client AfcExplorerWidget *explorer = new AfcExplorerWidget( - m_device, true, m_houseArrestAfcClient, "/Documents", this); + m_device, true, m_houseArrestAfcClient, false, "/Documents", this); explorer->setStyleSheet("border :none;"); m_containerLayout->addWidget(explorer); } diff --git a/src/iomanagerclient.cpp b/src/iomanagerclient.cpp index 4c73d9a..f31d093 100644 --- a/src/iomanagerclient.cpp +++ b/src/iomanagerclient.cpp @@ -30,7 +30,7 @@ IOManagerClient::IOManagerClient(QObject *parent) : QObject(parent) {} void IOManagerClient::startExport( const std::shared_ptr device, const QList &items, const QString &destinationPath, - const QString &exportTitle, std::optional altAfc) + const QString &exportTitle) { qDebug() << "startExport() entry - items:" << items.size() << "dest:" << destinationPath; @@ -72,6 +72,98 @@ void IOManagerClient::startExport( << "items"; } +/* hause_arrest */ +void IOManagerClient::startExport( + const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &exportTitle, const QString &bundleId) +{ + qDebug() << "startExport() hause_arrest entry - items:" << items.size() + << "dest:" << destinationPath; + if (!device) { + qWarning() << "Invalid device provided to ExportManager"; + QMessageBox::critical(nullptr, "Export Error", + "Invalid device specified for export."); + return; + } + + if (items.isEmpty()) { + qWarning() << "No items provided for export"; + QMessageBox::information(nullptr, "Export Error", + "No items selected for export."); + return; + } + + QDir destDir(destinationPath); + if (!destDir.exists()) { + if (!destDir.mkpath(".")) { + qWarning() << "Could not create destination directory:" + << destinationPath; + QMessageBox::critical(nullptr, "Export Error", + "Could not create destination directory."); + + return; + } + } + + QUuid jobId = QUuid::createUuid(); + + StatusBalloon::sharedInstance()->startProcess( + exportTitle, items.size(), destinationPath, ProcessType::Export, jobId); + + AppContext::sharedInstance()->ioManager->start_export_with_hause_arrest_afc( + device->udid, jobId, items, destinationPath, bundleId); + + qDebug() << "Started export job with hause_arrest_afc" << jobId << "for" + << items.size() << "items"; +} + +/* afc2 */ +void IOManagerClient::startExport( + const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &exportTitle, bool useAfc2) +{ + qDebug() << "startExport() hause_arrest entry - items:" << items.size() + << "dest:" << destinationPath; + if (!device) { + qWarning() << "Invalid device provided to ExportManager"; + QMessageBox::critical(nullptr, "Export Error", + "Invalid device specified for export."); + return; + } + + if (items.isEmpty()) { + qWarning() << "No items provided for export"; + QMessageBox::information(nullptr, "Export Error", + "No items selected for export."); + return; + } + + QDir destDir(destinationPath); + if (!destDir.exists()) { + if (!destDir.mkpath(".")) { + qWarning() << "Could not create destination directory:" + << destinationPath; + QMessageBox::critical(nullptr, "Export Error", + "Could not create destination directory."); + + return; + } + } + + QUuid jobId = QUuid::createUuid(); + + StatusBalloon::sharedInstance()->startProcess( + exportTitle, items.size(), destinationPath, ProcessType::Export, jobId); + + AppContext::sharedInstance()->ioManager->start_export_with_afc2( + device->udid, jobId, items, destinationPath); + + qDebug() << "Started export job with afc2" << jobId << "for" << items.size() + << "items"; +} + void IOManagerClient::startImport( const std::shared_ptr device, const QList &items, const QString &destinationPath, diff --git a/src/iomanagerclient.h b/src/iomanagerclient.h index 4f2107a..2ce186f 100644 --- a/src/iomanagerclient.h +++ b/src/iomanagerclient.h @@ -38,14 +38,22 @@ class IOManagerClient : public QObject public: static IOManagerClient *sharedInstance(); - // Delete copy and assignment operators IOManagerClient(const IOManagerClient &) = delete; IOManagerClient &operator=(const IOManagerClient &) = delete; + void startExport(const std::shared_ptr device, + const QList &items, + const QString &destinationPath, const QString &jobTitle); + /* afc2 */ void startExport(const std::shared_ptr device, const QList &items, const QString &destinationPath, const QString &jobTitle, - std::optional altAfc = std::nullopt); + bool useAfc2); + /* hause_arrest_afc*/ + void startExport(const std::shared_ptr device, + const QList &items, + const QString &destinationPath, const QString &exportTitle, + const QString &bundleId); void startImport(const std::shared_ptr device, const QList &items, diff --git a/src/rust/build.rs b/src/rust/build.rs index da8e4d4..3e73a93 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -4,6 +4,7 @@ fn main() { CxxQtBuilder::new() .file("src/lib.rs") .file("src/afc_services.rs") + .file("src/afc2_services.rs") .file("src/service_manager.rs") .file("src/screenshot.rs") .file("src/hause_arrest.rs") diff --git a/src/rust/src/afc2_services.rs b/src/rust/src/afc2_services.rs new file mode 100644 index 0000000..3f1934f --- /dev/null +++ b/src/rust/src/afc2_services.rs @@ -0,0 +1,781 @@ +use cxx_qt::Threading; +use cxx_qt_lib::{QByteArray, QList, QMap, QMapPair_QString_QVariant, QString}; + +use crate::{APP_DEVICE_STATE, RUNTIME, VIDEO_STREAMS, afc, run_sync}; +use idevice::{ + IdeviceService, + afc::{AfcClient, opcode::AfcFopenMode}, +}; +use once_cell::sync::Lazy; +use regex::Regex; +use std::{io::SeekFrom, pin::Pin}; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufWriter}; +use tokio::net::TcpListener; +use tokio::sync::{Semaphore, oneshot}; + +#[cxx_qt::bridge(namespace = "CXX")] +mod qobject { + #[namespace = ""] + unsafe extern "C++" { + include!("cxx-qt-lib/qstring.h"); + include!("cxx-qt-lib/qlist.h"); + include!("cxx-qt-lib/qbytearray.h"); + include!("cxx-qt-lib/qmap.h"); + include!("cxx-qt-lib/qvariant.h"); + + type QString = cxx_qt_lib::QString; + type QList_QString = cxx_qt_lib::QList; + type QByteArray = cxx_qt_lib::QByteArray; + type QMap_QString_QVariant = cxx_qt_lib::QMap; + } + + extern "RustQt" { + #[qobject] + type Afc2Backend = super::RAfc2Backend; + + #[qinvokable] + fn set_udid(self: Pin<&mut Afc2Backend>, udid: &QString); + + #[qinvokable] + fn load_album_list(self: Pin<&mut Afc2Backend>); + + #[qinvokable] + fn list_dir(self: &Afc2Backend, path: &QString) -> QList_QString; + + #[qinvokable] + fn file_to_buffer(self: &Afc2Backend, file_path: &QString) -> QByteArray; + + #[qinvokable] + fn is_directory(self: &Afc2Backend, path: &QString) -> bool; + + #[qinvokable] + fn get_file_size(self: &Afc2Backend, path: &QString) -> i64; + + #[qinvokable] + fn read_file_range(self: &Afc2Backend, path: &QString, offset: i64, len: i64) + -> QByteArray; + + #[qinvokable] + fn check_is_dir_and_list(self: &Afc2Backend, path: &QString); + #[qsignal] + fn check_is_dir_and_list_finished( + self: Pin<&mut Afc2Backend>, + success: bool, + entries: &QMap_QString_QVariant, + ); + + #[qsignal] + fn album_list_loaded(self: Pin<&mut Afc2Backend>, udid: QString, album_list: QList_QString); + + #[qinvokable] + fn get_dirs_item_count(self: &Afc2Backend, dir: &QList_QString) -> i64; + + #[qinvokable] + fn list_files_flat(self: &Afc2Backend, dir: &QString) -> QList_QString; + + #[qinvokable] + fn start_video_stream(self: &Afc2Backend, file_path: &QString) -> QString; + + #[qinvokable] + fn is_available(self: &Afc2Backend) -> bool; + } + + impl cxx_qt::Threading for Afc2Backend {} + impl cxx_qt::Constructor<(QString,), NewArguments = (QString,)> for Afc2Backend {} +} + +#[derive(Default)] +pub struct RAfc2Backend { + udid: QString, +} +impl cxx_qt::Constructor<(QString,)> for qobject::Afc2Backend { + type BaseArguments = (); + type InitializeArguments = (); + type NewArguments = (QString,); + + fn route_arguments( + args: (QString,), + ) -> ( + Self::NewArguments, + Self::BaseArguments, + Self::InitializeArguments, + ) { + (args, (), ()) + } + + fn new(args: (QString,)) -> RAfc2Backend { + RAfc2Backend { udid: args.0 } + } +} + +impl qobject::Afc2Backend { + fn get_udid(&self) -> &QString { + use cxx_qt::CxxQtType; + &self.rust().udid + } + + fn set_udid(mut self: Pin<&mut Self>, udid: &QString) { + use cxx_qt::CxxQtType; + self.as_mut().rust_mut().udid = udid.clone(); + } + + fn is_available(self: &Self) -> bool { + let udid_string = self.get_udid().to_string(); + + run_sync(async move { + let device = APP_DEVICE_STATE + .lock() + .await + .get(udid_string.as_str()) + .cloned(); + if let Some(d) = device { + d.afc2.is_some() + } else { + eprintln!("Device with UDID {} not found", udid_string); + false + } + }) + } + + fn is_directory(self: &Self, path: &QString) -> bool { + let udid_string = self.get_udid().to_string(); + let path_string = path.to_string(); + + run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE + .lock() + .await + .get(udid_string.as_str()) + .cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid_string); + return false; + } + }; + + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid_string); + return false; + } + } + }; + + let mut afc = afc_arc.lock().await; + match afc.get_file_info(path_string.clone()).await { + Ok(info) => info.st_ifmt == "S_IFDIR", + Err(e) => { + eprintln!("Failed to get file info for {path_string}: {e}"); + false + } + } + }) + } + + fn list_dir(self: &Self, path: &QString) -> QList { + let udid = self.get_udid().to_string(); + let path_str = path.to_string(); + let list = run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid); + return Vec::new(); + } + }; + + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid); + return Vec::new(); + } + } + }; + + let mut afc = afc_arc.lock().await; + match afc.list_dir(&path_str).await { + Ok(list) => list, + Err(e) => { + eprintln!("Failed to read directory {path_str}: {e}"); + Vec::new() + } + } + }); + + let mut qlist: QList = QList::default(); + for name in list { + qlist.append(QString::from(name)); + } + qlist + } + + fn check_is_dir_and_list(self: &Self, path: &QString) { + let udid = self.get_udid().to_string(); + let path_str = path.to_string(); + let qt_t = self.qt_thread(); + + RUNTIME.spawn(async move { + let qt_thread = qt_t.clone(); + let afc_arc = { + let device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let device = match device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid); + qt_thread + .queue(move |q| { + q.check_is_dir_and_list_finished( + false, + &QMap::::default(), + ); + }) + .ok(); + return; + } + }; + + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid); + qt_thread + .queue(move |q| { + q.check_is_dir_and_list_finished( + false, + &QMap::::default(), + ); + }) + .ok(); + return; + } + } + }; + + let mut afc = afc_arc.lock().await; + let map_result = afc::check_is_dir_and_list(&mut afc, path_str).await; + + qt_thread + .queue(move |q| { + q.check_is_dir_and_list_finished(true, &map_result); + }) + .ok(); + }); + } + + fn load_album_list(self: Pin<&mut Self>) { + let qt_t = self.qt_thread(); + let udid_owned = self.get_udid().clone(); + + RUNTIME.spawn(async move { + let udid_str = udid_owned.to_string(); + let afc_arc = { + let device = APP_DEVICE_STATE + .lock() + .await + .get(udid_str.as_str()) + .cloned(); + let device = match device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid_str); + return; + } + }; + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid_str); + return; + } + } + }; + + println!("Device found: {:?}", udid_str); + + // list entries in /DCIM + let mut afc = afc_arc.lock().await; + let album_names = match afc.list_dir("/DCIM").await { + Ok(list) => list, + Err(e) => { + eprintln!("Failed to load /DCIM directory: {e}"); + return; + } + }; + + // Regexes: ^\d{3}APPLE$ and ^\d{8}$ + static RE_3DIGIT_APPLE: Lazy = + Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap()); + static RE_DATE_YYYYMMDD: Lazy = Lazy::new(|| Regex::new(r"^\d{8}$").unwrap()); + + let mut qlist_album: QList = QList::default(); + + for name in album_names { + // skip . and .. + if name == "." || name == ".." { + continue; + } + + // name filter + let matches_name = name.contains("APPLE") + || RE_3DIGIT_APPLE.is_match(&name) + || RE_DATE_YYYYMMDD.is_match(&name); + + if !matches_name { + continue; + } + + // check it's a directory + let full_path = format!("/DCIM/{name}"); + match afc.get_file_info(full_path).await { + Ok(info) => { + if info.st_ifmt != "S_IFDIR" { + continue; + } + } + Err(_) => continue, + }; + + qlist_album.append(QString::from(name)); + } + let qt_thread = qt_t.clone(); + qt_thread + .queue(move |backend_qobj| { + backend_qobj.album_list_loaded(udid_owned.clone(), qlist_album); + }) + .unwrap(); + }); + } + + fn file_to_buffer(&self, album_path: &QString) -> QByteArray { + let udid = self.get_udid().to_string(); + let album_path_string = album_path.to_string(); + + let data: Vec = run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("file_to_buffer: device {udid} not found"); + return Vec::new(); + } + }; + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid); + return Vec::new(); + } + } + }; + + let mut afc = afc_arc.lock().await; + + let mut fd = match afc + .open(album_path_string.clone(), AfcFopenMode::RdOnly) + .await + { + Ok(f) => f, + Err(e) => { + eprintln!("file_to_buffer: failed to open {album_path_string}: {e}"); + return Vec::new(); + } + }; + + let mut buf = Vec::new(); + let mut chunk = vec![0u8; 8192]; + + loop { + let n = match fd.read(&mut chunk).await { + Ok(n) => n, + Err(e) => { + eprintln!("file_to_buffer: failed to read {album_path_string}: {e}"); + buf.clear(); + break; + } + }; + if n == 0 { + break; + } + buf.extend_from_slice(&chunk[..n]); + } + fd.close().await.ok(); + buf + }); + + if data.is_empty() { + QByteArray::default() + } else { + QByteArray::from(&data[..]) + } + } + + fn get_file_size(self: &Self, path: &QString) -> i64 { + let udid = self.get_udid().to_string(); + let path_string = path.to_string(); + + run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("file_to_buffer: device {udid} not found"); + return -1; + } + }; + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid); + return -1; + } + } + }; + + let mut afc = afc_arc.lock().await; + + afc::get_file_size(&mut afc, path_string) + .await + .map(|v| v as i64) + .unwrap_or(-1) + }) + } + + fn read_file_range(&self, path: &QString, offset: i64, len: i64) -> QByteArray { + if offset < 0 || len <= 0 { + return QByteArray::default(); + } + + let udid = self.get_udid().to_string(); + let path_string = path.to_string(); + + let data: Vec = run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("read_file_range: device {udid} not found"); + return Vec::new(); + } + }; + + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid); + return Vec::new(); + } + } + }; + + let mut afc = afc_arc.lock().await; + + let mut fd = match afc.open(path_string.clone(), AfcFopenMode::RdOnly).await { + Ok(f) => f, + Err(e) => { + eprintln!("read_file_range: open({path_string}) failed: {e}"); + return Vec::new(); + } + }; + + if offset > 0 { + if let Err(e) = fd.seek(SeekFrom::Start(offset as u64)).await { + eprintln!("read_file_range: seek({path_string}, {offset}) failed: {e}"); + let _ = fd.close().await; + return Vec::new(); + } + } + + let mut buf = Vec::new(); + let mut remaining = len as usize; + let mut chunk = vec![0u8; 8192]; + + while remaining > 0 { + let to_read = remaining.min(chunk.len()); + let n = match fd.read(&mut chunk[..to_read]).await { + Ok(n) => n, + Err(e) => { + eprintln!("read_file_range: read({path_string}) failed: {e}"); + buf.clear(); + break; + } + }; + if n == 0 { + break; + } + buf.extend_from_slice(&chunk[..n]); + remaining -= n; + } + + let _ = fd.close().await; + buf + }); + + if data.is_empty() { + QByteArray::default() + } else { + QByteArray::from(&data[..]) + } + } + + fn get_dirs_item_count(self: &Self, dirs: &QList) -> i64 { + let udid = self.get_udid().to_string(); + + let mut dir_vec: Vec = Vec::new(); + for i in 0..dirs.len() { + if let Some(qdir) = dirs.get(i) { + dir_vec.push(qdir.to_string()); + } + } + + run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("get_dirs_item_count: device {udid} not found"); + return -1; + } + }; + + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid); + return -1; + } + } + }; + + let mut afc = afc_arc.lock().await; + let mut total: i64 = 0; + + for dir_str in dir_vec { + let names = match afc.list_dir(&dir_str).await { + Ok(list) => list, + Err(e) => { + eprintln!("get_dirs_item_count: list_dir({dir_str}) failed: {e}"); + continue; + } + }; + + let count = names + .into_iter() + .filter(|name| name != "." && name != "..") + .count() as i64; + + total += count; + } + + total + }) + } + + fn list_files_flat(self: &Self, dir: &QString) -> QList { + let udid = self.get_udid().to_string(); + let dir_str = dir.to_string(); + + let entries = run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("list_files_flat: device {udid} not found"); + return Vec::new(); + } + }; + + match device.afc2 { + Some(afc) => afc.clone(), + None => { + eprintln!("AFC2 service not available for device {}", udid); + return Vec::new(); + } + } + }; + + let mut afc = afc_arc.lock().await; + + let names = match afc.list_dir(&dir_str).await { + Ok(list) => list, + Err(e) => { + eprintln!("list_files_flat: list_dir({dir_str}) failed: {e}"); + return Vec::new(); + } + }; + + let mut files = Vec::new(); + for name in names { + if name == "." || name == ".." { + continue; + } + let full_path = format!("{}/{}", dir_str, name); + + match afc.get_file_info(full_path.clone()).await { + Ok(info) => { + if info.st_ifmt != "S_IFDIR" { + files.push(full_path); + } + } + Err(e) => { + eprintln!("list_files_flat: get_file_info({full_path}) failed: {e}"); + continue; + } + } + } + files + }); + + let mut qlist: QList = QList::default(); + for path in entries { + qlist.append(QString::from(path)); + } + qlist + } + + fn start_video_stream(&self, file_path: &QString) -> QString { + let udid_str = self.get_udid().to_string(); + let path_str = file_path.to_string(); + let cloned_path = path_str.clone(); + + eprintln!( + "start_video_stream: request udid={} path={}", + udid_str, cloned_path + ); + + // bind ephemeral port on localhost + let listener = match std::net::TcpListener::bind("127.0.0.1:0") { + Ok(l) => l, + Err(e) => { + eprintln!("start_video_stream: bind failed: {e}"); + return QString::default(); + } + }; + let local_addr = match listener.local_addr() { + Ok(a) => a, + Err(e) => { + eprintln!("start_video_stream: local_addr failed: {e}"); + return QString::default(); + } + }; + listener.set_nonblocking(true).ok(); + + // create Tokio TcpListener inside runtime + let std_listener = { + let _guard = RUNTIME.handle().enter(); + match TcpListener::from_std(listener) { + Ok(l) => l, + Err(e) => { + eprintln!("start_video_stream: from_std failed: {e}"); + return QString::default(); + } + } + }; + + let port = local_addr.port(); + + let encoded = urlencoding::encode(&cloned_path); + let url = format!("http://127.0.0.1:{}/{}", port, encoded); + let url_clone = url.clone(); + let url_clone_for_log = url.clone(); + let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); + { + let mut map = VIDEO_STREAMS.lock().unwrap(); + map.insert(url.clone(), shutdown_tx); + } + eprintln!( + "start_video_stream: serving {} for udid={} path={}", + url_clone, udid_str, cloned_path + ); + // accept-loop task + RUNTIME.spawn(async move { + loop { + tokio::select! { + _ = &mut shutdown_rx => { + // shutdown requested + eprintln!("start_video_stream: shutdown requested for {}", url_clone); + break; + } + accept_res = std_listener.accept() => { + let (socket, peer) = match accept_res { + Ok(s) => s, + Err(e) => { + eprintln!("start_video_stream: accept error: {e} on {}", url_clone); + break; + } + }; + eprintln!("start_video_stream: accepted connection from {} on {}", peer, url_clone); + + let udid_clone = udid_str.clone(); + let path_clone = path_str.clone(); + + let mut afc_client = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(&udid_clone).cloned(); + let device = match maybe_device { + Some(d) => d, + None => { + // FIXME + // eprintln!( + // "handle_http_connection: device {} not found for {}", + // udid, path + // ); + // let _ = socket + // .write_all( + // b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + // ) + // .await; + // let _ = socket.shutdown().await; + return; + } + }; + let provider = device.provider.lock().await; + match AfcClient::new_afc2(provider.as_ref()).await { + Ok(c) => c, + Err(e) => { + //FIXME + // eprintln!( + // "handle_http_connection: AfcClient::connect failed for {}: {:?}", + // path, e + // ); + // let _ = socket + // .write_all( + // b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + // ) + // .await; + // let _ = socket.shutdown().await; + return; + } + } + }; + + + tokio::spawn(async move { + afc::handle_http_connection(&mut afc_client, path_clone, socket).await; + }); + } + } + } + eprintln!("start_video_stream: accept-loop exiting for {}", url_clone); + }); + + QString::from(url_clone_for_log) + } +} diff --git a/src/rust/src/afc_services.rs b/src/rust/src/afc_services.rs index 5ca042d..83845bf 100644 --- a/src/rust/src/afc_services.rs +++ b/src/rust/src/afc_services.rs @@ -114,28 +114,31 @@ impl qobject::AfcBackend { use cxx_qt::CxxQtType; self.as_mut().rust_mut().udid = udid.clone(); } + fn is_directory(self: &Self, path: &QString) -> bool { let udid_string = self.get_udid().to_string(); let path_string = path.to_string(); run_sync(async move { - // get device once - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid_string.as_str()) - .cloned(); + let afc_arc = { + let maybe_device = APP_DEVICE_STATE + .lock() + .await + .get(udid_string.as_str()) + .cloned(); - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid_string); - return false; - } + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid_string); + return false; + } + }; + + device.afc.clone() }; - // use AFC inside the same async block, no nested block_on - let mut afc = device.afc.lock().await; + let mut afc = afc_arc.lock().await; match afc.get_file_info(path_string.clone()).await { Ok(info) => info.st_ifmt == "S_IFDIR", Err(e) => { @@ -150,21 +153,26 @@ impl qobject::AfcBackend { let udid = self.get_udid().to_string(); let path_str = path.to_string(); let list = run_sync(async move { - let device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - let device = match device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid); - return Vec::new(); - } + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid); + return Vec::new(); + } + }; + + device.afc.clone() }; - match device.afc.lock().await.list_dir(&path_str).await { + let mut afc = afc_arc.lock().await; + match afc.list_dir(&path_str).await { Ok(list) => list, Err(e) => { eprintln!("Failed to read directory {path_str}: {e}"); - return Vec::new(); + Vec::new() } } }); @@ -182,26 +190,29 @@ impl qobject::AfcBackend { let qt_t = self.qt_thread(); RUNTIME.spawn(async move { - let device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); let qt_thread = qt_t.clone(); - let device = match device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid); - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished( - false, - &QMap::::default(), - ); - }) - .ok(); - return; - } + let afc = { + let device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let device = match device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid); + qt_thread + .queue(move |q| { + q.check_is_dir_and_list_finished( + false, + &QMap::::default(), + ); + }) + .ok(); + return; + } + }; + + device.afc.clone() }; - let mut afc = device.afc.lock().await; - + let mut afc = afc.lock().await; let map_result = afc::check_is_dir_and_list(&mut afc, path_str).await; qt_thread @@ -218,70 +229,75 @@ impl qobject::AfcBackend { RUNTIME.spawn(async move { let udid_str = udid_owned.to_string(); - let device = APP_DEVICE_STATE - .lock() - .await - .get(udid_str.as_str()) - .cloned(); - if let Some(device) = device { - println!("Device found: {:?}", udid_str); - - // list entries in /DCIM - let album_names = match device.afc.lock().await.list_dir("/DCIM").await { - Ok(list) => list, - Err(e) => { - eprintln!("Failed to load /DCIM directory: {e}"); + let afc_arc = { + let device = APP_DEVICE_STATE + .lock() + .await + .get(udid_str.as_str()) + .cloned(); + let device = match device { + Some(d) => d, + None => { + eprintln!("Device with UDID {} not found", udid_str); return; } }; + device.afc.clone() + }; - // Regexes: ^\d{3}APPLE$ and ^\d{8}$ - static RE_3DIGIT_APPLE: Lazy = - Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap()); - static RE_DATE_YYYYMMDD: Lazy = - Lazy::new(|| Regex::new(r"^\d{8}$").unwrap()); + println!("Device found: {:?}", udid_str); - let mut qlist_album: QList = QList::default(); + // list entries in /DCIM + let mut afc = afc_arc.lock().await; + let album_names = match afc.list_dir("/DCIM").await { + Ok(list) => list, + Err(e) => { + eprintln!("Failed to load /DCIM directory: {e}"); + return; + } + }; - for name in album_names { - // skip . and .. - if name == "." || name == ".." { - continue; - } + // Regexes: ^\d{3}APPLE$ and ^\d{8}$ + static RE_3DIGIT_APPLE: Lazy = + Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap()); + static RE_DATE_YYYYMMDD: Lazy = Lazy::new(|| Regex::new(r"^\d{8}$").unwrap()); - // name filter - let matches_name = name.contains("APPLE") - || RE_3DIGIT_APPLE.is_match(&name) - || RE_DATE_YYYYMMDD.is_match(&name); + let mut qlist_album: QList = QList::default(); - if !matches_name { - continue; - } - - // check it's a directory - let full_path = format!("/DCIM/{name}"); - match device.afc.lock().await.get_file_info(full_path).await { - Ok(info) => { - if info.st_ifmt != "S_IFDIR" { - continue; - } - } - Err(_) => continue, - }; - - qlist_album.append(QString::from(name)); + for name in album_names { + // skip . and .. + if name == "." || name == ".." { + continue; } - let qt_thread = qt_t.clone(); - qt_thread - .queue(move |backend_qobj| { - backend_qobj.album_list_loaded(udid_owned.clone(), qlist_album); - }) - .unwrap(); - } else { - eprintln!("Device with UDID {} not found", udid_str); - return; + // name filter + let matches_name = name.contains("APPLE") + || RE_3DIGIT_APPLE.is_match(&name) + || RE_DATE_YYYYMMDD.is_match(&name); + + if !matches_name { + continue; + } + + // check it's a directory + let full_path = format!("/DCIM/{name}"); + match afc.get_file_info(full_path).await { + Ok(info) => { + if info.st_ifmt != "S_IFDIR" { + continue; + } + } + Err(_) => continue, + }; + + qlist_album.append(QString::from(name)); } + let qt_thread = qt_t.clone(); + qt_thread + .queue(move |backend_qobj| { + backend_qobj.album_list_loaded(udid_owned.clone(), qlist_album); + }) + .unwrap(); }); } @@ -380,17 +396,21 @@ impl qobject::AfcBackend { let path_string = path.to_string(); let data: Vec = run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("read_file_range: device {udid} not found"); - return Vec::new(); - } + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("read_file_range: device {udid} not found"); + return Vec::new(); + } + }; + + device.afc.clone() }; - let mut afc = device.afc.lock().await; + let mut afc = afc_arc.lock().await; let mut fd = match afc.open(path_string.clone(), AfcFopenMode::RdOnly).await { Ok(f) => f, @@ -451,17 +471,21 @@ impl qobject::AfcBackend { } run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("get_dirs_item_count: device {udid} not found"); - return -1; - } + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("get_dirs_item_count: device {udid} not found"); + return -1; + } + }; + + device.afc.clone() }; - let mut afc = device.afc.lock().await; + let mut afc = afc_arc.lock().await; let mut total: i64 = 0; for dir_str in dir_vec { @@ -490,17 +514,21 @@ impl qobject::AfcBackend { let dir_str = dir.to_string(); let entries = run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("list_files_flat: device {udid} not found"); - return Vec::new(); - } + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("list_files_flat: device {udid} not found"); + return Vec::new(); + } + }; + + device.afc.clone() }; - let mut afc = device.afc.lock().await; + let mut afc = afc_arc.lock().await; let names = match afc.list_dir(&dir_str).await { Ok(list) => list, diff --git a/src/rust/src/hause_arrest.rs b/src/rust/src/hause_arrest.rs index e9d592d..255eeee 100644 --- a/src/rust/src/hause_arrest.rs +++ b/src/rust/src/hause_arrest.rs @@ -31,6 +31,9 @@ mod qobject { #[qobject] type HauseArrest = super::RHauseArrestBackend; + #[qinvokable] + fn get_bundle_id(self: &HauseArrest) -> &QString; + #[qinvokable] fn init_session(self: Pin<&mut HauseArrest>); #[qsignal] @@ -95,6 +98,12 @@ impl qobject::HauseArrest { use cxx_qt::CxxQtType; &self.rust().udid } + + fn get_bundle_id(&self) -> &QString { + use cxx_qt::CxxQtType; + &self.rust().bundle_id + } + fn init_session(self: Pin<&mut Self>) { let udid_str = self.udid.to_string(); let bundle_id_str = self.bundle_id.to_string(); diff --git a/src/rust/src/io_manager.rs b/src/rust/src/io_manager.rs index dfc9cfe..1f83919 100644 --- a/src/rust/src/io_manager.rs +++ b/src/rust/src/io_manager.rs @@ -39,6 +39,25 @@ mod qobject { destination_dir: &QString, ); + #[qinvokable] + fn start_export_with_afc2( + self: Pin<&mut IOManager>, + udid: &QString, + job_id: &QUuid, + device_paths: &QList_QString, + destination_dir: &QString, + ); + + #[qinvokable] + fn start_export_with_hause_arrest_afc( + self: Pin<&mut IOManager>, + udid: &QString, + job_id: &QUuid, + device_paths: &QList_QString, + destination_dir: &QString, + hause_arrest_afc: &QString, + ); + #[qinvokable] fn start_import( self: Pin<&mut IOManager>, @@ -300,7 +319,7 @@ impl qobject::IOManager { job_id: &qobject::QUuid, device_paths: &qobject::QList_QString, destination_dir: &qobject::QString, - hause_arrest_afc: &String, + hause_arrest_afc: &qobject::QString, ) { let udid_str = udid.to_string(); let dest_dir_str = destination_dir.to_string(); diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 1566db8..92c601b 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -30,6 +30,7 @@ use once_cell::sync::Lazy; use plist::Value; mod afc; mod afc_services; +mod afc2_services; mod hause_arrest; mod io_manager; mod screenshot; @@ -38,32 +39,22 @@ mod utils; const POSSIBLE_ROOT: &str = "../../../../"; const APP_LABEL: &str = "iDescriptor"; +const EV_CONNECTED: u32 = 1; +const EV_DISCONNECTED: u32 = 2; +const EV_PAIRING_PENDING: u32 = 3; +const EV_FAIL_KIND: u32 = 4; -// #[derive(Clone)] +#[derive(Clone)] pub struct DeviceServices { pub afc: Arc>, pub afc2: Option>>, pub diag: Arc>, - pub heartbeat_task: Option>, + pub heartbeat_task: Option>>, pub video_streams: Arc>>>, pub provider: Arc>>, pub lockdown: Arc>, } -impl Clone for DeviceServices { - fn clone(&self) -> Self { - DeviceServices { - afc: self.afc.clone(), - afc2: self.afc2.clone(), - diag: self.diag.clone(), - // FIXME? - heartbeat_task: None, - video_streams: self.video_streams.clone(), - provider: self.provider.clone(), - lockdown: self.lockdown.clone(), - } - } -} pub static APP_DEVICE_STATE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); @@ -205,27 +196,39 @@ impl qobject::Core { APP_LABEL, ); - // FIXME: return if fails - let info: (String, String) = - init_idescriptor_device( - provider, - qt_thread.clone(), - ) - .await - .unwrap_or_default(); + let info = init_idescriptor_device( + provider, + qt_thread.clone(), + ) + .await; - let udid_for_event = info.0.clone(); - let info_for_event = info.1.clone(); - - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - 1, - &QString::from(udid_for_event), - &QString::from(info_for_event), - ); - }) - .ok(); + match info { + Some((udid_for_event, info_for_event)) => { + qt_thread + .queue(move |core_qobj| { + core_qobj.device_event( + EV_CONNECTED, + &QString::from( + udid_for_event, + ), + &QString::from( + info_for_event, + ), + ); + }) + .ok(); + } + // FIXME: sometimes happens + /* + init_idescriptor_device: Attempting to start Lockdown session. + init_idescriptor_device: Lockdown session started. + init_idescriptor_device: Attempting to get default values from Lockdown. + init_idescriptor_device: Default values obtained. + init_idescriptor_device: Attempting to connect to AFC client. + AfcClient::connect failed: PasswordProtected + */ + None => return, + } } if already_paired { @@ -233,19 +236,36 @@ impl qobject::Core { return; } + fn emit_pairing_failed( + qt_thread: cxx_qt::CxxQtThread, + udid: String, + reason : &str, + ) { + let reason_clone = reason.to_string(); + qt_thread + .queue(move |core_qobj| { + core_qobj.device_event( + EV_FAIL_KIND, + &QString::from(udid), + // FIXME: reason is not info + &QString::from(reason_clone), + ); + }) + .ok(); + } + // pairing pending let udid_for_event = udid.clone(); qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - 3, + .queue(move |core_qobj| { + core_qobj.device_event( + EV_PAIRING_PENDING, &QString::from(udid_for_event), &QString::from(""), ); }) .ok(); - let ev_fail_kind = 4; let mut uc2 = match UsbmuxdConnection::default() .await @@ -253,15 +273,7 @@ impl qobject::Core { Ok(u) => u, Err(_) => { let udid_for_event = udid.clone(); - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - ev_fail_kind, - &QString::from(udid_for_event), - &QString::from(""), - ); - }) - .unwrap(); + emit_pairing_failed(qt_thread.clone(), udid_for_event, "Failed to connect to usbmuxd"); return; } }; @@ -270,23 +282,13 @@ impl qobject::Core { Ok(d) => d, Err(_) => { let udid_for_event = udid.clone(); - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - ev_fail_kind, - &QString::from(udid_for_event), - &QString::from(""), - ); - }) - .unwrap(); + emit_pairing_failed(qt_thread.clone(), udid_for_event, "Failed to get device from usbmuxd"); return; } }; - let provider = dev.to_provider( - UsbmuxdAddr::default(), - "iDescriptor", - ); + let provider = dev + .to_provider(UsbmuxdAddr::default(), APP_LABEL); let mut lc = match LockdownClient::connect( &provider, @@ -296,15 +298,7 @@ impl qobject::Core { Ok(l) => l, Err(_) => { let udid_for_event = udid.clone(); - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - ev_fail_kind, - &QString::from(udid_for_event), - &QString::from(""), - ); - }) - .unwrap(); + emit_pairing_failed(qt_thread.clone(), udid_for_event, "Failed to connect to Lockdown"); return; } }; @@ -313,15 +307,7 @@ impl qobject::Core { Ok(b) => b, Err(_) => { let udid_for_event = udid.clone(); - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - ev_fail_kind, - &QString::from(udid_for_event), - &QString::from(""), - ); - }) - .unwrap(); + emit_pairing_failed(qt_thread.clone(), udid_for_event, "Failed to get BUID from usbmuxd"); return; } }; @@ -343,15 +329,9 @@ impl qobject::Core { Ok(p) => p, Err(_) => { let udid_for_event = udid.clone(); - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - ev_fail_kind, - &QString::from(udid_for_event), - &QString::from(""), - ); - }) - .unwrap(); + // FIXME: we may not want to emit here + // because if user doesnt accept pairing in time, it will be considered a failure + emit_pairing_failed(qt_thread.clone(), udid_for_event, "Failed to pair device"); return; } }; @@ -361,15 +341,7 @@ impl qobject::Core { Ok(b) => b, Err(_) => { let udid_for_event = udid.clone(); - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - ev_fail_kind, - &QString::from(udid_for_event), - &QString::from(""), - ); - }) - .unwrap(); + emit_pairing_failed(qt_thread.clone(), udid_for_event, "Failed to serialize pairing file"); return; } }; @@ -378,15 +350,7 @@ impl qobject::Core { uc2.save_pair_record(&udid, bytes).await { let udid_for_event = udid.clone(); - qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - ev_fail_kind, - &QString::from(udid_for_event), - &QString::from(""), - ); - }) - .unwrap(); + emit_pairing_failed(qt_thread.clone(), udid_for_event, "Failed to save pairing record to usbmuxd"); return; } @@ -400,14 +364,14 @@ impl qobject::Core { let qt_thread = qt_t.clone(); qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - 2, + .queue(move |core_qobj| { + core_qobj.device_event( + EV_DISCONNECTED, &QString::from(udid), &QString::from(""), ); }) - .unwrap(); + .ok(); } } Err(e) => { @@ -448,26 +412,33 @@ impl qobject::Core { Err(e) => { let qt_thread = qt_t.clone(); qt_thread - .queue(move |Core_qobj| { - Core_qobj.no_pairing_file(&QString::from(mac_address_owned)); + .queue(move |core_qobj| { + core_qobj.no_pairing_file(&QString::from(mac_address_owned)); }) - .unwrap(); + .ok(); eprintln!("Failed to read pairing file: {e}"); return; } }; - let t = TcpProvider { - addr: ip_owned.parse::().unwrap(), - pairing_file: pairing_file, - label: APP_LABEL.to_string(), + let addr = match ip_owned.parse::() { + Ok(addr) => addr, + Err(e) => { + //FIXME: emit event for failure + eprintln!("Invalid IP address {}: {}", ip_owned, e); + return; + } + }; + + let t = TcpProvider { + addr, + pairing_file, + label: APP_LABEL.to_string(), }; - let result = tokio::select! { - res = init_idescriptor_device(t, qt_t.clone()) => { - res - } + let result = tokio::select! { + res = init_idescriptor_device(t, qt_t.clone()) => res, // timeout _ = tokio::time::sleep(tokio::time::Duration::from_secs(10)) => { eprintln!("Timeout collecting device info for wireless device mac address: {mac_address_owned}"); @@ -481,9 +452,9 @@ impl qobject::Core { let qt_thread = qt_t.clone(); qt_thread - .queue(move |Core_qobj| { - Core_qobj.device_event( - 1, + .queue(move |core_qobj| { + core_qobj.device_event( + EV_CONNECTED, &QString::from(udid), &QString::from(info), ); @@ -495,8 +466,8 @@ impl qobject::Core { let qt_thread = qt_t.clone(); qt_thread - .queue(move |Core_qobj| { - Core_qobj.init_failed(&QString::from(mac_address_owned)); + .queue(move |core_qobj| { + core_qobj.init_failed(&QString::from(mac_address_owned)); }) .ok(); } @@ -666,7 +637,7 @@ async fn init_idescriptor_device< if is_wireless { let mut hb_for_task = hb.take().unwrap(); let udid_for_hb = udid.clone(); - hb_task = Some(RUNTIME.spawn(async move { + hb_task = Some(Arc::new(RUNTIME.spawn(async move { eprintln!("heartbeat: starting heartbeat task "); let mut interval = 15u64; let mut fails = 0; @@ -686,9 +657,9 @@ async fn init_idescriptor_device< clean_device_from_app_state(&udid_for_hb).await; let udid_for_event = udid_for_hb.clone(); - let _ = qt_thread_for_hb.queue(move |Core_qobj| { - Core_qobj.device_event( - 2, + let _ = qt_thread_for_hb.queue(move |core_qobj| { + core_qobj.device_event( + EV_DISCONNECTED, &QString::from(udid_for_event), &QString::from(""), ); @@ -708,9 +679,9 @@ async fn init_idescriptor_device< clean_device_from_app_state(&udid_for_hb).await; let udid_for_event = udid_for_hb.clone(); - let _ = qt_thread_for_hb.queue(move |Core_qobj| { - Core_qobj.device_event( - 2, + let _ = qt_thread_for_hb.queue(move |core_qobj| { + core_qobj.device_event( + EV_DISCONNECTED, &QString::from(udid_for_event), &QString::from(""), ); @@ -725,7 +696,7 @@ async fn init_idescriptor_device< } eprintln!("heartbeat: heartbeat task ended."); - })); + }))); } // FIXME: this cannot be done when paired wirelessly diff --git a/src/rust/src/utils.rs b/src/rust/src/utils.rs index 47ca076..558a02c 100644 --- a/src/rust/src/utils.rs +++ b/src/rust/src/utils.rs @@ -1,7 +1,7 @@ use crate::POSSIBLE_ROOT; use cxx_qt_lib::QString; use idevice::{ - IdeviceService, afc::AfcClient, diagnostics_relay::DiagnosticsRelayClient, + IdeviceError, IdeviceService, afc::AfcClient, diagnostics_relay::DiagnosticsRelayClient, house_arrest::HouseArrestClient, installation_proxy::InstallationProxyClient, provider::IdeviceProvider, }; @@ -10,6 +10,8 @@ use plist_macro::plist; use rusqlite::Connection; use std::path::PathBuf; +pub const PUBLIC_STAGING: &str = "PublicStaging"; + pub async fn get_battery_info(diag: &mut DiagnosticsRelayClient) -> Option { match diag.ioregistry(None, None, Some("IOPMPowerSource")).await { Ok(Some(dict)) => Some(dict), @@ -28,7 +30,7 @@ pub async fn get_cable_info(diag: &mut DiagnosticsRelayClient) -> Option bool { - match afc.list_dir(POSSIBLE_ROOT).await { + match afc.list_dir(format!("{}/bin", POSSIBLE_ROOT)).await { Ok(vec) => vec.len() > 0, Err(_) => false, } @@ -146,3 +148,12 @@ pub fn get_lockdown_path() -> PathBuf { base.join("Apple").join("Lockdown") } } + +/// Ensure `PublicStaging` exists on device via AFC +pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { + // Try to stat and if it fails, create directory + match afc.get_file_info(PUBLIC_STAGING).await { + Ok(_) => Ok(()), + Err(_) => afc.mk_dir(PUBLIC_STAGING).await, + } +}