fix: use hause_arrest or afc2 client whenever possible

This commit is contained in:
uncor3
2026-04-04 10:32:55 +00:00
parent 491f1af88c
commit 56fa9310a6
19 changed files with 1315 additions and 346 deletions
+71 -46
View File
@@ -45,11 +45,11 @@
AfcExplorerWidget::AfcExplorerWidget(
const std::shared_ptr<iDescriptorDevice> device, bool favEnabled,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest, QString root,
QWidget *parent)
std::optional<std::shared_ptr<CXX::HauseArrest>> 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<ExportItem> 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<QListWidgetItem *> 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<ExportItem> exportItems;
// QString currPath = "/";
// if (!m_history.isEmpty())
// currPath = m_history.top();
// if (!currPath.endsWith("/"))
// currPath += "/";
QList<QString> 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 <AFC2>",
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,
+6 -1
View File
@@ -22,6 +22,7 @@
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "iomanagerclient.h"
#include "zloadingwidget.h"
#include <QAction>
#include <QEvent>
@@ -50,7 +51,8 @@ public:
bool favEnabled = false,
std::optional<std::shared_ptr<CXX::HauseArrest>>
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<QString, QVariant> &entries);
void handleExport(QList<QListWidgetItem *> filesToExport);
#ifndef WIN32
void updateNavStyles();
+5 -10
View File
@@ -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<iDescriptorDevice> 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<iDescriptorDevice>(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
+1 -1
View File
@@ -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);
+8 -10
View File
@@ -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);
+2
View File
@@ -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);
+24 -7
View File
@@ -63,14 +63,15 @@ void ImageLoader::requestThumbnail(
void ImageLoader::requestImageWithCallback(
const std::shared_ptr<iDescriptorDevice> device, const QString &path,
int priority, std::function<void(const QPixmap &)> callback,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest)
std::optional<std::shared_ptr<CXX::HauseArrest>> 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<iDescriptorDevice> device, const QString &filePath,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest)
std::optional<std::shared_ptr<CXX::HauseArrest>> 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<iDescriptorDevice> device, const QString &filePath,
const QSize &size,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest)
std::optional<std::shared_ptr<CXX::HauseArrest>> 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<iDescriptorDevice> device, const QString &filePath,
const QSize &requestedSize,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest)
std::optional<std::shared_ptr<CXX::HauseArrest>> 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);
+8 -4
View File
@@ -31,7 +31,8 @@ public:
const std::shared_ptr<iDescriptorDevice> device, const QString &path,
int priority, std::function<void(const QPixmap &)> callback,
std::optional<std::shared_ptr<CXX::HauseArrest>> 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<iDescriptorDevice> device,
const QString &filePath, const QSize &size,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest =
std::nullopt);
std::nullopt,
bool useAfc2 = false);
static QPixmap generateVideoThumbnailFFmpeg(
const std::shared_ptr<iDescriptorDevice> device,
const QString &filePath, const QSize &size,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest =
std::nullopt);
std::nullopt,
bool useAfc2 = false);
static QPixmap loadImage(const std::shared_ptr<iDescriptorDevice> device,
const QString &filePath,
std::optional<std::shared_ptr<CXX::HauseArrest>>
hause_arrest = std::nullopt);
hause_arrest = std::nullopt,
bool useAfc2 = false);
signals:
void thumbnailReady(const QString &path, const QPixmap &image,
unsigned int row);
+9 -6
View File
@@ -18,9 +18,10 @@ public:
ImageTask(const std::shared_ptr<iDescriptorDevice> device,
const QString &path, unsigned int row, bool scale = true,
std::optional<std::shared_ptr<CXX::HauseArrest>> 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<std::shared_ptr<CXX::HauseArrest>> m_hause_arrest;
bool m_useAfc2;
};
#endif // IMAGETASK_H
+3 -3
View File
@@ -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<QString, QVariant> &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);
}
+93 -1
View File
@@ -30,7 +30,7 @@ IOManagerClient::IOManagerClient(QObject *parent) : QObject(parent) {}
void IOManagerClient::startExport(
const std::shared_ptr<iDescriptorDevice> device,
const QList<QString> &items, const QString &destinationPath,
const QString &exportTitle, std::optional<bool> 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<iDescriptorDevice> device,
const QList<QString> &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<iDescriptorDevice> device,
const QList<QString> &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<iDescriptorDevice> device,
const QList<QString> &items, const QString &destinationPath,
+10 -2
View File
@@ -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<iDescriptorDevice> device,
const QList<QString> &items,
const QString &destinationPath, const QString &jobTitle);
/* afc2 */
void startExport(const std::shared_ptr<iDescriptorDevice> device,
const QList<QString> &items,
const QString &destinationPath, const QString &jobTitle,
std::optional<bool> altAfc = std::nullopt);
bool useAfc2);
/* hause_arrest_afc*/
void startExport(const std::shared_ptr<iDescriptorDevice> device,
const QList<QString> &items,
const QString &destinationPath, const QString &exportTitle,
const QString &bundleId);
void startImport(const std::shared_ptr<iDescriptorDevice> device,
const QList<QString> &items,
+1
View File
@@ -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")
+781
View File
@@ -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<QString>;
type QByteArray = cxx_qt_lib::QByteArray;
type QMap_QString_QVariant = cxx_qt_lib::QMap<cxx_qt_lib::QMapPair_QString_QVariant>;
}
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<QString> {
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<QString> = 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::<QMapPair_QString_QVariant>::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::<QMapPair_QString_QVariant>::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<Regex> =
Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap());
static RE_DATE_YYYYMMDD: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{8}$").unwrap());
let mut qlist_album: QList<QString> = 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<u8> = 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<u8> = 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<QString>) -> i64 {
let udid = self.get_udid().to_string();
let mut dir_vec: Vec<String> = 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<QString> {
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<QString> = 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)
}
}
+145 -117
View File
@@ -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::<QMapPair_QString_QVariant>::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::<QMapPair_QString_QVariant>::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<Regex> =
Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap());
static RE_DATE_YYYYMMDD: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\d{8}$").unwrap());
println!("Device found: {:?}", udid_str);
let mut qlist_album: QList<QString> = 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<Regex> =
Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap());
static RE_DATE_YYYYMMDD: Lazy<Regex> = 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<QString> = 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<u8> = 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,
+9
View File
@@ -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();
+20 -1
View File
@@ -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();
+106 -135
View File
@@ -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<Mutex<AfcClient>>,
pub afc2: Option<Arc<Mutex<AfcClient>>>,
pub diag: Arc<Mutex<DiagnosticsRelayClient>>,
pub heartbeat_task: Option<JoinHandle<()>>,
pub heartbeat_task: Option<Arc<JoinHandle<()>>>,
pub video_streams: Arc<Mutex<HashMap<String, Arc<AtomicBool>>>>,
pub provider: Arc<Mutex<Box<dyn idevice::provider::IdeviceProvider>>>,
pub lockdown: Arc<Mutex<LockdownClient>>,
}
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<Mutex<HashMap<String, DeviceServices>>> =
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<Core>,
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::<IpAddr>().unwrap(),
pairing_file: pairing_file,
label: APP_LABEL.to_string(),
let addr = match ip_owned.parse::<IpAddr>() {
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
+13 -2
View File
@@ -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<PlistDictionary> {
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<PlistDi
}
pub async fn detect_jailbroken(afc: &mut AfcClient) -> 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,
}
}