From dc46b8ecfe10f951dfb78ee05b0ae461d2d5024f Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Tue, 21 Sep 2021 18:33:27 +0300 Subject: [PATCH] Assets delivery (#26) * Add GZipUncompressor class * Add skeleton for TarFile class * Add zlib develpment package to Dockerfile * Tar archive parser & indexer * Set native text rendering * Skeleton framework for CLI storage interface * Serial port commands cleanup * Add Failable and MOTDSkipper classes * MOTDSkipper is now an operation * Add libgl-dev package to docker * Remove unnecessary symlink in Docker * Add remove operation * Saving a lot of work * GZipUncompressor cleanup & improvements * Better manifest parsing, verification & debug * Fix error while upgrading FUS * Working file tree operations * Add WriteOperation * Improved correctness * Add MkDir operation * Revert to chunk-based read operation * Working asset update from file * Improved error handling * Add message feedback * Fix compilation on Windows --- application/application.cpp | 5 +- .../components/FlipperListDelegate.qml | 6 + application/screens/homescreen.qml | 11 + backend/abstractoperation.cpp | 23 +- backend/abstractoperation.h | 16 +- backend/abstractserialoperation.cpp | 43 ++ backend/abstractserialoperation.h | 29 ++ backend/backend.pro | 30 ++ backend/deviceregistry.cpp | 1 - backend/failable.h | 28 ++ backend/filenode.cpp | 211 ++++++++++ backend/filenode.h | 62 +++ backend/firmwaredownloader.cpp | 14 +- backend/firmwaredownloader.h | 2 + backend/flipperzero/assetmanifest.cpp | 121 ++++++ backend/flipperzero/assetmanifest.h | 46 +++ .../flipperzero/common/skipmotdoperation.cpp | 25 ++ .../flipperzero/common/skipmotdoperation.h | 23 ++ backend/flipperzero/deviceinfofetcher.cpp | 161 +++++--- backend/flipperzero/deviceinfofetcher.h | 36 +- backend/flipperzero/flipperzero.cpp | 54 ++- backend/flipperzero/flipperzero.h | 6 + .../operations/assetsdownloadoperation.cpp | 379 ++++++++++++++++++ .../operations/assetsdownloadoperation.h | 66 +++ .../operations/firmwaredownloadoperation.cpp | 2 +- .../operations/firmwaredownloadoperation.h | 2 +- .../operations/fixbootissuesoperation.cpp | 2 +- .../operations/fixbootissuesoperation.h | 2 +- .../operations/fixoptionbytesoperation.cpp | 2 +- .../operations/fixoptionbytesoperation.h | 2 +- .../wirelessstackdownloadoperation.cpp | 2 +- .../wirelessstackdownloadoperation.h | 2 +- backend/flipperzero/recoverycontroller.cpp | 15 +- backend/flipperzero/recoverycontroller.h | 9 +- backend/flipperzero/remotecontroller.cpp | 5 +- .../flipperzero/storage/mkdiroperation.cpp | 49 +++ backend/flipperzero/storage/mkdiroperation.h | 27 ++ backend/flipperzero/storage/readoperation.cpp | 127 ++++++ backend/flipperzero/storage/readoperation.h | 43 ++ .../flipperzero/storage/removeoperation.cpp | 50 +++ backend/flipperzero/storage/removeoperation.h | 27 ++ backend/flipperzero/storage/statoperation.cpp | 132 ++++++ backend/flipperzero/storage/statoperation.h | 49 +++ .../flipperzero/storage/writeoperation.cpp | 122 ++++++ backend/flipperzero/storage/writeoperation.h | 44 ++ backend/flipperzero/storagecontroller.cpp | 126 ++++++ backend/flipperzero/storagecontroller.h | 59 +++ backend/gzipuncompressor.cpp | 105 +++++ backend/gzipuncompressor.h | 33 ++ backend/qflipperbackend.cpp | 4 + backend/signalingfailable.h | 22 + backend/simpleserialoperation.cpp | 68 ++++ backend/simpleserialoperation.h | 35 ++ backend/tararchive.cpp | 128 ++++++ backend/tararchive.h | 33 ++ qflipper_common.pri | 7 +- 56 files changed, 2566 insertions(+), 167 deletions(-) create mode 100644 backend/abstractserialoperation.cpp create mode 100644 backend/abstractserialoperation.h create mode 100644 backend/failable.h create mode 100644 backend/filenode.cpp create mode 100644 backend/filenode.h create mode 100644 backend/flipperzero/assetmanifest.cpp create mode 100644 backend/flipperzero/assetmanifest.h create mode 100644 backend/flipperzero/common/skipmotdoperation.cpp create mode 100644 backend/flipperzero/common/skipmotdoperation.h create mode 100644 backend/flipperzero/operations/assetsdownloadoperation.cpp create mode 100644 backend/flipperzero/operations/assetsdownloadoperation.h create mode 100644 backend/flipperzero/storage/mkdiroperation.cpp create mode 100644 backend/flipperzero/storage/mkdiroperation.h create mode 100644 backend/flipperzero/storage/readoperation.cpp create mode 100644 backend/flipperzero/storage/readoperation.h create mode 100644 backend/flipperzero/storage/removeoperation.cpp create mode 100644 backend/flipperzero/storage/removeoperation.h create mode 100644 backend/flipperzero/storage/statoperation.cpp create mode 100644 backend/flipperzero/storage/statoperation.h create mode 100644 backend/flipperzero/storage/writeoperation.cpp create mode 100644 backend/flipperzero/storage/writeoperation.h create mode 100644 backend/flipperzero/storagecontroller.cpp create mode 100644 backend/flipperzero/storagecontroller.h create mode 100644 backend/gzipuncompressor.cpp create mode 100644 backend/gzipuncompressor.h create mode 100644 backend/signalingfailable.h create mode 100644 backend/simpleserialoperation.cpp create mode 100644 backend/simpleserialoperation.h create mode 100644 backend/tararchive.cpp create mode 100644 backend/tararchive.h diff --git a/application/application.cpp b/application/application.cpp index 160731dd..96e323d1 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -1,11 +1,9 @@ #include "application.h" -#include #include -#include #include #include -#include +#include #include #include "qflipperbackend.h" @@ -39,6 +37,7 @@ AppUpdater *Application::updater() void Application::initStyles() { + QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); QQuickStyle::setStyle("Universal"); } diff --git a/application/components/FlipperListDelegate.qml b/application/components/FlipperListDelegate.qml index cc1acea0..e871722e 100644 --- a/application/components/FlipperListDelegate.qml +++ b/application/components/FlipperListDelegate.qml @@ -6,6 +6,7 @@ Item { signal localUpdateRequested(var device) signal localRadioUpdateRequested(var device) signal localFUSUpdateRequested(var device) + signal localAssetsUpdateRequested(var device) signal fixOptionBytesRequested(var device) signal fixBootRequested(var device) @@ -185,6 +186,11 @@ Item { Menu { title: qsTr("Expert options") + MenuItem { + text: qsTr("Update Databases...") + onTriggered: localAssetsUpdateRequested(device) + } + MenuItem { text: qsTr("Update Wireless stack...") onTriggered: localRadioUpdateRequested(device) diff --git a/application/screens/homescreen.qml b/application/screens/homescreen.qml index c352eb29..8d84152e 100644 --- a/application/screens/homescreen.qml +++ b/application/screens/homescreen.qml @@ -127,6 +127,17 @@ Item { }, messageObj); } + onLocalAssetsUpdateRequested: { + const messageObj = { + title : qsTr("Update the databases?"), + subtitle : qsTr("This will install the databases from a file.") + }; + + fileDialog.openWithConfirmation(["Database files (*.tgz)", "All files (*)"], function() { + downloader.downloadAssets(device, fileDialog.fileUrl); + }, messageObj); + } + onLocalRadioUpdateRequested: { const messageObj = { title : qsTr("Update the wireless stack?"), diff --git a/backend/abstractoperation.cpp b/backend/abstractoperation.cpp index 8bf165d5..e8693e07 100644 --- a/backend/abstractoperation.cpp +++ b/backend/abstractoperation.cpp @@ -6,8 +6,6 @@ AbstractOperation::AbstractOperation(QObject *parent): QObject(parent), - m_isError(false), - m_errorString(QStringLiteral("No error")), m_timeout(new QTimer(this)), m_state(BasicState::Ready) { @@ -22,19 +20,9 @@ int AbstractOperation::state() const return m_state; } -bool AbstractOperation::isError() const -{ - return m_isError; -} - -const QString &AbstractOperation::errorString() const -{ - return m_errorString; -} - void AbstractOperation::onOperationTimeout() { - finishWithError(QStringLiteral("Operation timeout (generic).")); + finishWithError(QStringLiteral("Operation timeout (generic)")); } void AbstractOperation::setState(int state) @@ -42,14 +30,9 @@ void AbstractOperation::setState(int state) m_state = state; } -void AbstractOperation::finishWithError(const QString &errorString) +void AbstractOperation::finishWithError(const QString &errorMsg) { - error_msg(errorString); - - m_isError = true; - m_errorString = errorString; - - setState(BasicState::Finished); + setError(errorMsg); finish(); } diff --git a/backend/abstractoperation.h b/backend/abstractoperation.h index 2c356a18..a16a31f0 100644 --- a/backend/abstractoperation.h +++ b/backend/abstractoperation.h @@ -2,9 +2,12 @@ #include +#include "failable.h" + class QTimer; -class AbstractOperation: public QObject { +class AbstractOperation: public QObject, public Failable +{ Q_OBJECT public: @@ -17,14 +20,11 @@ class AbstractOperation: public QObject { explicit AbstractOperation(QObject *parent = nullptr); virtual ~AbstractOperation() {} - virtual const QString name() const = 0; + virtual const QString description() const = 0; virtual void start() = 0; virtual void finish() = 0; int state() const; - bool isError() const; - const QString &errorString() const; - signals: void started(); void finished(); @@ -34,14 +34,12 @@ protected slots: protected: void setState(int state); - void finishWithError(const QString &errorString); + void finishWithError(const QString &errorMsg); - void startTimeout(int msec = 10000); + void startTimeout(int msec = 5000); void stopTimeout(); private: - bool m_isError; - QString m_errorString; QTimer *m_timeout; int m_state; }; diff --git a/backend/abstractserialoperation.cpp b/backend/abstractserialoperation.cpp new file mode 100644 index 00000000..a261afaf --- /dev/null +++ b/backend/abstractserialoperation.cpp @@ -0,0 +1,43 @@ +#include "abstractserialoperation.h" + +#include +#include + +#include "macros.h" + +AbstractSerialOperation::AbstractSerialOperation(QSerialPort *serialPort, QObject *parent): + AbstractOperation(parent), + m_serialPort(serialPort) +{} + +AbstractSerialOperation::~AbstractSerialOperation() +{} + +void AbstractSerialOperation::start() +{ + connect(m_serialPort, &QSerialPort::readyRead, this, &AbstractSerialOperation::onSerialPortReadyRead); + connect(m_serialPort, &QSerialPort::errorOccurred, this, &AbstractSerialOperation::onSerialPortError); + + QTimer::singleShot(0, this, &AbstractSerialOperation::begin); +} + +void AbstractSerialOperation::finish() +{ + stopTimeout(); + setState(BasicState::Finished); + + disconnect(m_serialPort, &QSerialPort::readyRead, this, &AbstractSerialOperation::onSerialPortReadyRead); + disconnect(m_serialPort, &QSerialPort::errorOccurred, this, &AbstractSerialOperation::onSerialPortError); + + emit finished(); +} + +QSerialPort *AbstractSerialOperation::serialPort() const +{ + return m_serialPort; +} + +void AbstractSerialOperation::onSerialPortError() +{ + finishWithError(m_serialPort->errorString()); +} diff --git a/backend/abstractserialoperation.h b/backend/abstractserialoperation.h new file mode 100644 index 00000000..5c45d21e --- /dev/null +++ b/backend/abstractserialoperation.h @@ -0,0 +1,29 @@ +#pragma once + +#include "abstractoperation.h" + +class QSerialPort; + +class AbstractSerialOperation : public AbstractOperation +{ + Q_OBJECT + +public: + AbstractSerialOperation(QSerialPort *serialPort, QObject *parent = nullptr); + virtual ~AbstractSerialOperation(); + + void start() override; + void finish() override; + +protected: + QSerialPort *serialPort() const; + +private slots: + virtual void onSerialPortReadyRead() = 0; + void onSerialPortError(); + +private: + virtual bool begin() = 0; + + QSerialPort *m_serialPort; +}; diff --git a/backend/backend.pro b/backend/backend.pro index cc031a97..e96f5652 100644 --- a/backend/backend.pro +++ b/backend/backend.pro @@ -10,12 +10,17 @@ include(../qflipper_common.pri) SOURCES += \ abstractoperation.cpp \ + abstractserialoperation.cpp \ deviceregistry.cpp \ + filenode.cpp \ firmwaredownloader.cpp \ flipperupdates.cpp \ + flipperzero/assetmanifest.cpp \ + flipperzero/common/skipmotdoperation.cpp \ flipperzero/deviceinfofetcher.cpp \ flipperzero/factoryinfo.cpp \ flipperzero/flipperzero.cpp \ + flipperzero/operations/assetsdownloadoperation.cpp \ flipperzero/operations/firmwaredownloadoperation.cpp \ flipperzero/operations/fixbootissuesoperation.cpp \ flipperzero/operations/fixoptionbytesoperation.cpp \ @@ -23,20 +28,35 @@ SOURCES += \ flipperzero/operations/wirelessstackdownloadoperation.cpp \ flipperzero/recoverycontroller.cpp \ flipperzero/remotecontroller.cpp \ + flipperzero/storage/mkdiroperation.cpp \ + flipperzero/storage/readoperation.cpp \ + flipperzero/storage/removeoperation.cpp \ + flipperzero/storage/statoperation.cpp \ + flipperzero/storage/writeoperation.cpp \ + flipperzero/storagecontroller.cpp \ + gzipuncompressor.cpp \ qflipperbackend.cpp \ remotefilefetcher.cpp \ serialfinder.cpp \ + simpleserialoperation.cpp \ + tararchive.cpp \ updateregistry.cpp HEADERS += \ abstractoperation.h \ + abstractserialoperation.h \ deviceregistry.h \ + failable.h \ + filenode.h \ firmwaredownloader.h \ flipperupdates.h \ + flipperzero/assetmanifest.h \ + flipperzero/common/skipmotdoperation.h \ flipperzero/deviceinfo.h \ flipperzero/deviceinfofetcher.h \ flipperzero/factoryinfo.h \ flipperzero/flipperzero.h \ + flipperzero/operations/assetsdownloadoperation.h \ flipperzero/operations/firmwaredownloadoperation.h \ flipperzero/operations/fixbootissuesoperation.h \ flipperzero/operations/fixoptionbytesoperation.h \ @@ -44,9 +64,19 @@ HEADERS += \ flipperzero/operations/wirelessstackdownloadoperation.h \ flipperzero/recoverycontroller.h \ flipperzero/remotecontroller.h \ + flipperzero/storage/mkdiroperation.h \ + flipperzero/storage/readoperation.h \ + flipperzero/storage/removeoperation.h \ + flipperzero/storage/statoperation.h \ + flipperzero/storage/writeoperation.h \ + flipperzero/storagecontroller.h \ + gzipuncompressor.h \ qflipperbackend.h \ remotefilefetcher.h \ serialfinder.h \ + signalingfailable.h \ + simpleserialoperation.h \ + tararchive.h \ updateregistry.h unix|win32 { diff --git a/backend/deviceregistry.cpp b/backend/deviceregistry.cpp index 33d2146a..06ddbcf5 100644 --- a/backend/deviceregistry.cpp +++ b/backend/deviceregistry.cpp @@ -50,7 +50,6 @@ void DeviceRegistry::insertDevice(const USBDeviceInfo &info) if(info.vendorID() == FLIPPER_ZERO_VID) { auto *fetcher = Zero::AbstractDeviceInfoFetcher::create(info, this); connect(fetcher, &Zero::AbstractDeviceInfoFetcher::finished, this, &DeviceRegistry::processDevice); - fetcher->fetch(); } else { error_msg("Unexpected device VID and PID."); diff --git a/backend/failable.h b/backend/failable.h new file mode 100644 index 00000000..332379ee --- /dev/null +++ b/backend/failable.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +class Failable +{ + bool m_isError; + QString m_errorString; + +public: + Failable() { + resetError(); + } + + bool isError() const { return m_isError; } + const QString &errorString() const { return m_errorString; } + + virtual void setError(const QString &errorMessage) { + m_isError = true; + m_errorString = errorMessage; + } + + void resetError() { + m_isError = false; + m_errorString = QStringLiteral("No Error"); + } +}; + diff --git a/backend/filenode.cpp b/backend/filenode.cpp new file mode 100644 index 00000000..22fe9639 --- /dev/null +++ b/backend/filenode.cpp @@ -0,0 +1,211 @@ +#include "filenode.h" + +#include + +#include "macros.h" + +FileNode::FileNode(): + m_parent(nullptr) +{} + +FileNode::FileNode(const QString &name, Type type, const QVariant &data): + m_parent(nullptr), + m_info({name, QString(), type, data}) +{} + +bool FileNode::operator ==(const FileNode &other) const +{ + return (m_info.type == other.m_info.type) && + (m_info.absolutePath == other.m_info.absolutePath) && + (m_info.userData == other.m_info.userData); +} + +bool FileNode::operator !=(const FileNode &other) const +{ + return !(*this == other); +} + +const QString &FileNode::name() const +{ + return m_info.name; +} + +const QString &FileNode::absolutePath() const +{ + return m_info.absolutePath; +} + +FileNode::Type FileNode::type() const +{ + return m_info.type; +} + +const QVariant &FileNode::userData() const +{ + return m_info.userData; +} + +const FileNode::FileInfo &FileNode::fileInfo() const +{ + return m_info; +} + +void FileNode::addChild(const QSharedPointer &nodePtr) +{ + nodePtr->setParent(this); + m_children.insert(nodePtr->name(), nodePtr); +} + +bool FileNode::addDirectory(const QString &path) +{ + auto fragments = path.split('/'); + QSharedPointer node(new FileNode(fragments.takeLast(), Type::Directory)); + + auto *parent = traverse(fragments); + check_return_bool(parent, QStringLiteral("No parent node found for %1.").arg(path)); + + parent->addChild(node); + return true; +} + +bool FileNode::addFile(const QString &path, const QVariant &data) +{ + auto fragments = path.split('/'); + QSharedPointer node(new FileNode(fragments.takeLast(), Type::RegularFile, data)); + + auto *parent = traverse(fragments); + check_return_bool(parent, QStringLiteral("No parent node found for %1.").arg(path)); + + parent->addChild(node); + return true; +} + +FileNode *FileNode::traverse(const QStringList &fragments) +{ + if(fragments == QStringList({QString()})) { + return this; + } + + auto *current = this; + + for(const auto &fragment: fragments) { + current = current->child(fragment); + + if(!current) { + break; + } + } + + return current; +} + +FileNode *FileNode::child(const QString &name) const +{ + if(!m_children.contains(name)) { + return nullptr; + } + + return m_children.value(name).get(); +} + +FileNode *FileNode::find(const QString &path) +{ + const auto fragments = path.split('/'); + return traverse(fragments); +} + +FileNode *FileNode::parent() const +{ + return m_parent; +} + +FileNode::FileInfoList FileNode::toPreOrderList() const +{ + FileInfoList ret; + QQueue queue; + + queue.enqueue(this); + + while(!queue.isEmpty()) { + const auto *current = queue.dequeue(); + + for(const auto &childPtr: current->m_children) { + queue.enqueue(childPtr.get()); + } + + ret.append(current->m_info); + } + + return ret; +} + +FileNode::FileInfoList FileNode::difference(FileNode *other) +{ + FileInfoList ret; + QQueue queue; + + queue.enqueue(other); + + while(!queue.isEmpty()) { + auto *current = queue.dequeue(); + auto *counterpart = find(current->absolutePath()); + + if(!counterpart || (counterpart->type() != current->type())) { + ret.append(current->toPreOrderList()); + continue; + } + + for(const auto &childPtr: qAsConst(current->m_children)) { + queue.enqueue(childPtr.get()); + } + } + + return ret; +} + +FileNode::FileInfoList FileNode::changed(FileNode *other) +{ + FileInfoList ret; + QQueue queue; + + queue.enqueue(other); + + while(!queue.isEmpty()) { + auto *current = queue.dequeue(); + auto *counterpart = find(current->absolutePath()); + + if(!counterpart || (counterpart->type() != current->type())) { + continue; + + } else if (*current != *counterpart) { + ret.append(current->m_info); + continue; + } + + for(const auto &childPtr: qAsConst(current->m_children)) { + queue.enqueue(childPtr.get()); + } + } + + return ret; +} + +void FileNode::setParent(FileNode *node) +{ + m_parent = node; + QStringList fragments; + + auto *current = this; + while(current->parent()) { + fragments.append(current->name()); + current = current->parent(); + } + + std::reverse(fragments.begin(), fragments.end()); + m_info.absolutePath = fragments.join('/'); +} + +bool FileNode::FileInfo::operator <(const FileInfo &other) const +{ + return (type < other.type) || (absolutePath < other.absolutePath); +} diff --git a/backend/filenode.h b/backend/filenode.h new file mode 100644 index 00000000..c9e734ee --- /dev/null +++ b/backend/filenode.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include + +class FileNode +{ +public: + enum class Type { + Directory, + RegularFile, + Unknown + }; + + struct FileInfo { + QString name; + QString absolutePath; + Type type; + QVariant userData; + + bool operator <(const FileInfo &other) const; + }; + + using FileNodeMap = QMap>; + using FileInfoList = QList; + + FileNode(); + FileNode(const QString &name, Type type, const QVariant &data = QVariant()); + + bool operator ==(const FileNode &other) const; + bool operator !=(const FileNode &other) const; + + const QString &name() const; + const QString &absolutePath() const; + Type type() const; + const QVariant &userData() const; + const FileInfo &fileInfo() const; + + bool addDirectory(const QString &path); + bool addFile(const QString &path, const QVariant &data); + + FileNode *parent() const; + FileNode *child(const QString &name) const; + + FileNode *find(const QString &path); + + FileInfoList toPreOrderList() const; + FileInfoList difference(FileNode *other); + FileInfoList changed(FileNode *other); + +private: + void setParent(FileNode *node); + void addChild(const QSharedPointer &nodePtr); + FileNode *traverse(const QStringList &fragments); + + FileNode *m_parent; + FileNodeMap m_children; + FileInfo m_info; +}; diff --git a/backend/firmwaredownloader.cpp b/backend/firmwaredownloader.cpp index b99f2ccb..6cc4c7bb 100644 --- a/backend/firmwaredownloader.cpp +++ b/backend/firmwaredownloader.cpp @@ -9,6 +9,7 @@ #include "flipperzero/operations/wirelessstackdownloadoperation.h" #include "flipperzero/operations/firmwaredownloadoperation.h" #include "flipperzero/operations/fixoptionbytesoperation.h" +#include "flipperzero/operations/assetsdownloadoperation.h" #include "flipperzero/operations/fixbootissuesoperation.h" #include "remotefilefetcher.h" @@ -81,6 +82,14 @@ void FirmwareDownloader::fixOptionBytes(FlipperZero *device, const QString &file enqueueOperation(new Flipper::Zero::FixOptionBytesOperation(device, file)); } +void FirmwareDownloader::downloadAssets(FlipperZero *device, const QString &filePath) +{ + const auto localUrl = QUrl(filePath).toLocalFile(); + auto *file = new QFile(localUrl, this); + + enqueueOperation(new Flipper::Zero::AssetsDownloadOperation(device, file, this)); +} + void FirmwareDownloader::processQueue() { if(m_operationQueue.isEmpty()) { @@ -88,12 +97,10 @@ void FirmwareDownloader::processQueue() return; } - m_state = State::Running; - auto *currentOperation = m_operationQueue.dequeue(); connect(currentOperation, &AbstractOperation::finished, this, [=]() { - info_msg(QStringLiteral("Operation '%1' finished with status: %2.").arg(currentOperation->name(), currentOperation->errorString())); + info_msg(QStringLiteral("Operation '%1' finished with status: %2.").arg(currentOperation->description(), currentOperation->errorString())); currentOperation->deleteLater(); processQueue(); }); @@ -107,6 +114,7 @@ void FirmwareDownloader::enqueueOperation(AbstractOperation *op) if(m_state == State::Ready) { // Leave the context before calling processQueue() + m_state = State::Running; QTimer::singleShot(20, this, &FirmwareDownloader::processQueue); } } diff --git a/backend/firmwaredownloader.h b/backend/firmwaredownloader.h index 371452f9..6deccdfe 100644 --- a/backend/firmwaredownloader.h +++ b/backend/firmwaredownloader.h @@ -34,6 +34,8 @@ public slots: void fixBootIssues(Flipper::FlipperZero *device); void fixOptionBytes(Flipper::FlipperZero *device, const QString &filePath); + void downloadAssets(Flipper::FlipperZero *device, const QString &filePath); + private slots: void processQueue(); diff --git a/backend/flipperzero/assetmanifest.cpp b/backend/flipperzero/assetmanifest.cpp new file mode 100644 index 00000000..84696339 --- /dev/null +++ b/backend/flipperzero/assetmanifest.cpp @@ -0,0 +1,121 @@ +#include "assetmanifest.h" + +#include + +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +AssetManifest::AssetManifest(): + m_version(-1), + m_timestamp(0), + m_root(new FileNode("", FileNode::Type::Directory)) +{} + +AssetManifest::AssetManifest(const QByteArray &text): + AssetManifest() +{ + QTextStream s(text); + + auto n = 1; + while(!s.atEnd()) { + if(!parseLine(s.readLine())) { + setError(QStringLiteral("Syntax error on line %1").arg(n++)); + return; + } + } + + if((m_version == -1) || (m_timestamp == 0)) { + setError(QStringLiteral("Incomplete manifest file")); + } +} + +int AssetManifest::version() const +{ + return m_version; +} + +time_t AssetManifest::timestamp() const +{ + return m_timestamp; +} + +FileNode *AssetManifest::tree() const +{ + return m_root.get(); +} + +bool AssetManifest::parseLine(const QString &line) +{ + const auto tokens = line.trimmed().split(':'); + return parseFile(tokens) || parseDirectory(tokens) || + parseVersion(tokens) || parseTime(tokens); +} + +bool AssetManifest::parseVersion(const QStringList &tokens) +{ + if((tokens.first() != "V") || (tokens.size() < 2)) { + return false; + } + + bool success; + const auto version = tokens[1].toInt(&success); + + if(success) { + m_version = version; + } + + return success; +} + +bool AssetManifest::parseTime(const QStringList &tokens) +{ + if((tokens.first() != "T") || (tokens.size() < 2)) { + return false; + } + + bool success; + const auto time = tokens[1].toLongLong(&success, 10); + + if(success) { + m_timestamp = time; + } + + return success; +} + +bool AssetManifest::parseFile(const QStringList &tokens) +{ + if((tokens.first() != "F") || (tokens.size() < 4)) { + return false; + } + + bool success; + FileInfo info; + + info.md5 = tokens[1].toLocal8Bit(); + info.size = tokens[2].toLongLong(&success, 10); + + return success && m_root->addFile(tokens[3], QVariant::fromValue(info)); +} + +bool AssetManifest::parseDirectory(const QStringList &tokens) +{ + if((tokens.first() != "D") || (tokens.size() < 2)) { + return false; + } + + const auto dirName = tokens[1].endsWith('/') ? tokens[1].chopped(1) : tokens[1]; + return m_root->addDirectory(dirName); +} + +bool AssetManifest::FileInfo::operator ==(const FileInfo &other) const +{ + return (size == other.size) && (md5 == other.md5); +} + +bool AssetManifest::FileInfo::operator <(const FileInfo &other) const +{ + return size < other.size; +} diff --git a/backend/flipperzero/assetmanifest.h b/backend/flipperzero/assetmanifest.h new file mode 100644 index 00000000..1cb922e5 --- /dev/null +++ b/backend/flipperzero/assetmanifest.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "filenode.h" +#include "failable.h" + +namespace Flipper { +namespace Zero { + +class AssetManifest : public Failable +{ +public: + struct FileInfo { + qint64 size; + QByteArray md5; + + bool operator ==(const FileInfo &other) const; + bool operator <(const FileInfo &other) const; + }; + + AssetManifest(); + AssetManifest(const QByteArray &text); + + int version() const; + time_t timestamp() const; + FileNode *tree() const; + +private: + bool parseLine(const QString &line); + bool parseVersion(const QStringList &tokens); + bool parseTime(const QStringList &tokens); + bool parseFile(const QStringList &tokens); + bool parseDirectory(const QStringList &tokens); + + int m_version; + time_t m_timestamp; + QSharedPointer m_root; +}; + +} +} + +Q_DECLARE_METATYPE(Flipper::Zero::AssetManifest::FileInfo) diff --git a/backend/flipperzero/common/skipmotdoperation.cpp b/backend/flipperzero/common/skipmotdoperation.cpp new file mode 100644 index 00000000..8041b1fb --- /dev/null +++ b/backend/flipperzero/common/skipmotdoperation.cpp @@ -0,0 +1,25 @@ +#include "skipmotdoperation.h" + +#include + +using namespace Flipper; +using namespace Zero; + +SkipMOTDOperation::SkipMOTDOperation(QSerialPort *serialPort, QObject *parent): + SimpleSerialOperation(serialPort, parent) +{} + +const QString SkipMOTDOperation::description() const +{ + return QStringLiteral("Skip MOTD @%1").arg(QString(serialPort()->portName())); +} + +QByteArray SkipMOTDOperation::endOfMessageToken() const +{ + return QByteArrayLiteral("\r\n>: "); +} + +uint32_t SkipMOTDOperation::flags() const +{ + return DTR; +} diff --git a/backend/flipperzero/common/skipmotdoperation.h b/backend/flipperzero/common/skipmotdoperation.h new file mode 100644 index 00000000..60f5552d --- /dev/null +++ b/backend/flipperzero/common/skipmotdoperation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "simpleserialoperation.h" + +namespace Flipper { +namespace Zero { + +class SkipMOTDOperation : public SimpleSerialOperation +{ + Q_OBJECT + +public: + SkipMOTDOperation(QSerialPort *serialPort, QObject *parent = nullptr); + const QString description() const override; + +protected: + QByteArray endOfMessageToken() const override; + uint32_t flags() const override; +}; + +} +} + diff --git a/backend/flipperzero/deviceinfofetcher.cpp b/backend/flipperzero/deviceinfofetcher.cpp index 44f6f33d..88bbd241 100644 --- a/backend/flipperzero/deviceinfofetcher.cpp +++ b/backend/flipperzero/deviceinfofetcher.cpp @@ -1,21 +1,26 @@ #include "deviceinfofetcher.h" #include +#include #include +#include "common/skipmotdoperation.h" #include "device/stm32wb55.h" #include "serialfinder.h" #include "factoryinfo.h" #include "macros.h" +#define RESPONSE_TIMEOUT_MS 5000 +#define PROMPT_READY "\r\n>: \a" + using namespace Flipper; using namespace Zero; AbstractDeviceInfoFetcher::AbstractDeviceInfoFetcher(QObject *parent): - QObject(parent), - m_isError(false), - m_errorString(QStringLiteral("No error")) -{} + QObject(parent) +{ + QTimer::singleShot(0, this, &AbstractDeviceInfoFetcher::fetch); +} AbstractDeviceInfoFetcher::~AbstractDeviceInfoFetcher() {} @@ -35,28 +40,20 @@ AbstractDeviceInfoFetcher *AbstractDeviceInfoFetcher::create(const USBDeviceInfo return nullptr; } -bool AbstractDeviceInfoFetcher::isError() const -{ - return m_isError; -} - -const QString &AbstractDeviceInfoFetcher::errorString() const +void AbstractDeviceInfoFetcher::finishWithError(const QString &errorString) { - return m_errorString; -} - -void AbstractDeviceInfoFetcher::setError(const QString &errorString) -{ - m_isError = true; - m_errorString = errorString; - - emit finished(); + setError(errorString); + finish(); } VCPDeviceInfoFetcher::VCPDeviceInfoFetcher(const USBDeviceInfo &info, QObject *parent): - AbstractDeviceInfoFetcher(parent) + AbstractDeviceInfoFetcher(parent), + m_responseTimer(new QTimer(this)), + m_serialPort(nullptr) { m_deviceInfo.usbInfo = info; + m_responseTimer->setSingleShot(true); + connect(m_responseTimer, &QTimer::timeout, this, &VCPDeviceInfoFetcher::onResponseTimeout); } void VCPDeviceInfoFetcher::fetch() @@ -73,43 +70,80 @@ const DeviceInfo &VCPDeviceInfoFetcher::result() const void VCPDeviceInfoFetcher::onSerialPortFound(const QSerialPortInfo &portInfo) { if(portInfo.isNull()) { - setError(QStringLiteral("Invalid serial port info.")); + finishWithError(QStringLiteral("Invalid serial port info.")); return; } m_deviceInfo.serialInfo = portInfo; + m_serialPort = new QSerialPort(portInfo, this); - auto *serialPort = new QSerialPort(portInfo, this); - - if(!serialPort->open(QIODevice::ReadWrite)) { - setError(serialPort->errorString()); + if(!m_serialPort->open(QIODevice::ReadWrite)) { + finishWithError(m_serialPort->errorString()); return; } - auto *timeout = new QTimer(this); + auto *skipper = new SkipMOTDOperation(m_serialPort, this); - connect(timeout, &QTimer::timeout, this, &AbstractDeviceInfoFetcher::finished); - connect(this, &AbstractDeviceInfoFetcher::finished, serialPort, &QSerialPort::close); + connect(skipper, &AbstractOperation::finished, this, [=]() { - connect(serialPort, &QSerialPort::errorOccurred, this, [=]() { - timeout->stop(); - setError(serialPort->errorString()); - }); - - connect(serialPort, &QSerialPort::readyRead, this, [=]() { - timeout->start(50); + if(skipper->isError()) { + finishWithError(skipper->errorString()); + } else if(m_serialPort->write("\rdevice_info\r\n") < 0) { + finishWithError(m_serialPort->errorString()); + } else { + connect(m_serialPort, &QSerialPort::readyRead, this, &VCPDeviceInfoFetcher::onSerialPortReadyRead); + connect(m_serialPort, &QSerialPort::errorOccurred, this, &VCPDeviceInfoFetcher::onSerialPortErrorOccured); - while(serialPort->canReadLine()) { - parseLine(serialPort->readLine()); + m_responseTimer->start(RESPONSE_TIMEOUT_MS); } + + skipper->deleteLater(); }); - const auto success = serialPort->setDataTerminalReady(true) && - (serialPort->write("\rdevice_info\r") > 0) && - serialPort->flush(); - if(success) { - timeout->start(5000); + skipper->start(); +} + +void VCPDeviceInfoFetcher::onSerialPortReadyRead() +{ + m_responseTimer->start(RESPONSE_TIMEOUT_MS); + m_receivedData += m_serialPort->readAll(); + + if(m_receivedData.endsWith(PROMPT_READY)) { + parseReceivedData(); + finish(); + } +} + +void VCPDeviceInfoFetcher::onSerialPortErrorOccured() +{ + finishWithError(m_serialPort->errorString()); +} + +void VCPDeviceInfoFetcher::onResponseTimeout() +{ + finishWithError(QStringLiteral("Operation timeout")); +} + +void VCPDeviceInfoFetcher::finish() +{ + m_responseTimer->stop(); + emit finished(); +} + +void VCPDeviceInfoFetcher::parseReceivedData() +{ + QBuffer buf(&m_receivedData, this); + + if(!buf.open(QIODevice::ReadOnly)) { + finishWithError(buf.errorString()); + return; } + + while(buf.canReadLine()) { + parseLine(buf.readLine()); + } + + buf.close(); } void VCPDeviceInfoFetcher::parseLine(const QByteArray &line) @@ -141,32 +175,30 @@ DFUDeviceInfoFetcher::DFUDeviceInfoFetcher(const USBDeviceInfo &info, QObject *p void DFUDeviceInfoFetcher::fetch() { - QTimer::singleShot(0, this, [=]() { - STM32WB55 device(m_deviceInfo.usbInfo); + STM32WB55 device(m_deviceInfo.usbInfo); - if(!device.beginTransaction()) { - setError(QStringLiteral("Failed to initiate transaction")); - return; - } + if(!device.beginTransaction()) { + finishWithError(QStringLiteral("Failed to initiate transaction")); + return; + } - const FactoryInfo factoryInfo(device.OTPData(FactoryInfo::size())); + const FactoryInfo factoryInfo(device.OTPData(FactoryInfo::size())); - if(!device.endTransaction()) { - setError(QStringLiteral("Failed to end transaction")); - return; - } + if(!device.endTransaction()) { + finishWithError(QStringLiteral("Failed to end transaction")); + return; + } - if(!factoryInfo.isValid()) { - setError(QStringLiteral("Failed to read device factory information")); - return; - } + if(!factoryInfo.isValid()) { + finishWithError(QStringLiteral("Failed to read device factory information")); + return; + } - m_deviceInfo.name = factoryInfo.name(); - m_deviceInfo.target = QStringLiteral("f%1").arg(factoryInfo.target()); - m_deviceInfo.color = (DeviceInfo::Color)factoryInfo.color(); + m_deviceInfo.name = factoryInfo.name(); + m_deviceInfo.target = QStringLiteral("f%1").arg(factoryInfo.target()); + m_deviceInfo.color = (DeviceInfo::Color)factoryInfo.color(); - emit finished(); - }); + finish(); } const DeviceInfo &DFUDeviceInfoFetcher::result() const @@ -174,3 +206,8 @@ const DeviceInfo &DFUDeviceInfoFetcher::result() const return m_deviceInfo; } +void DFUDeviceInfoFetcher::finish() +{ + emit finished(); +} + diff --git a/backend/flipperzero/deviceinfofetcher.h b/backend/flipperzero/deviceinfofetcher.h index 2b21a8bc..a6862ca9 100644 --- a/backend/flipperzero/deviceinfofetcher.h +++ b/backend/flipperzero/deviceinfofetcher.h @@ -1,16 +1,19 @@ #pragma once #include +#include +#include "failable.h" #include "deviceinfo.h" #include "usbdeviceinfo.h" +class QTimer; class QSerialPort; namespace Flipper { namespace Zero { -class AbstractDeviceInfoFetcher : public QObject +class AbstractDeviceInfoFetcher : public QObject, public Failable { Q_OBJECT @@ -19,22 +22,17 @@ class AbstractDeviceInfoFetcher : public QObject virtual ~AbstractDeviceInfoFetcher(); static AbstractDeviceInfoFetcher *create(const USBDeviceInfo &info, QObject *parent = nullptr); - - bool isError() const; - const QString &errorString() const; - - virtual void fetch() = 0; virtual const DeviceInfo &result() const = 0; signals: void finished(); -protected: - void setError(const QString &errorString); +protected slots: + virtual void fetch() = 0; -private: - bool m_isError; - QString m_errorString; +protected: + virtual void finish() = 0; + void finishWithError(const QString &errorString); }; class VCPDeviceInfoFetcher : public AbstractDeviceInfoFetcher @@ -43,16 +41,23 @@ class VCPDeviceInfoFetcher : public AbstractDeviceInfoFetcher public: VCPDeviceInfoFetcher(const USBDeviceInfo &info, QObject *parent = nullptr); - - void fetch() override; const DeviceInfo &result() const override; private slots: + void fetch() override; void onSerialPortFound(const QSerialPortInfo &portInfo); + void onSerialPortReadyRead(); + void onSerialPortErrorOccured(); + void onResponseTimeout(); private: + void finish() override; + void parseReceivedData(); void parseLine(const QByteArray &line); + QTimer *m_responseTimer; + QSerialPort *m_serialPort; + QByteArray m_receivedData; DeviceInfo m_deviceInfo; }; @@ -62,11 +67,14 @@ class DFUDeviceInfoFetcher : public AbstractDeviceInfoFetcher public: DFUDeviceInfoFetcher(const USBDeviceInfo &info, QObject *parent = nullptr); + const DeviceInfo &result() const override; +private slots: void fetch() override; - const DeviceInfo &result() const override; private: + void finish() override; + DeviceInfo m_deviceInfo; }; diff --git a/backend/flipperzero/flipperzero.cpp b/backend/flipperzero/flipperzero.cpp index 43c600c2..4e0740f7 100644 --- a/backend/flipperzero/flipperzero.cpp +++ b/backend/flipperzero/flipperzero.cpp @@ -4,7 +4,9 @@ #include #include "recoverycontroller.h" +#include "storagecontroller.h" #include "remotecontroller.h" + #include "macros.h" namespace Flipper { @@ -22,7 +24,8 @@ FlipperZero::FlipperZero(const Zero::DeviceInfo &info, QObject *parent): m_progress(0), m_remote(nullptr), - m_recovery(nullptr) + m_recovery(nullptr), + m_storage(nullptr) { initControllers(); } @@ -102,26 +105,14 @@ bool FlipperZero::bootToDFU() auto *serialPort = new QSerialPort(m_deviceInfo.serialInfo, this); - const auto portSuccess = serialPort->open(QIODevice::WriteOnly) && serialPort->setDataTerminalReady(true) && - (serialPort->write(QByteArrayLiteral("\rdfu\r")) > 0); - - // TODO: Is it necessary here? Why was this added? - auto flushTries = 30; - - while(--flushTries && !serialPort->flush()) { - info_msg("Serial port flush failure, retrying..."); - QThread::msleep(15); - } - - serialPort->close(); - - const auto success = portSuccess && flushTries; - + const auto success = serialPort->open(QIODevice::WriteOnly) && serialPort->setDataTerminalReady(true) && + (serialPort->write(QByteArrayLiteral("\rdfu\r\n")) > 0) && serialPort->waitForBytesWritten(1000); if(!success) { setError("Can't detach the device: Failed to reset in DFU mode"); error_msg(QString("Serial port status: %1").arg(serialPort->errorString())); } + serialPort->close(); serialPort->deleteLater(); return success; @@ -183,6 +174,11 @@ RecoveryController *FlipperZero::recovery() const return m_recovery; } +StorageController *FlipperZero::storage() const +{ + return m_storage; +} + void FlipperZero::setName(const QString &name) { if(m_deviceInfo.name == name) { @@ -230,6 +226,17 @@ void FlipperZero::setProgress(double progress) emit progressChanged(); } +void FlipperZero::onControllerErrorOccured() +{ + auto *controller = qobject_cast(sender()); + + if(!controller) { + return; + } + + setError(controller->errorString()); +} + void FlipperZero::initControllers() { if(m_remote) { @@ -242,23 +249,30 @@ void FlipperZero::initControllers() m_recovery = nullptr; } + if(m_storage) { + m_storage->deleteLater(); + m_storage = nullptr; + } + // TODO: better message delivery system if(isDFU()) { m_recovery = new RecoveryController(m_deviceInfo.usbInfo, this); + connect(m_recovery, &RecoveryController::messageChanged, this, [=]() { setMessage(m_recovery->message()); }); - connect(m_recovery, &RecoveryController::errorOccured, this, [=]() { - setError(m_recovery->errorString()); - }); - connect(m_recovery, &RecoveryController::progressChanged, this, [=]() { setProgress(m_recovery->progress()); }); + connect(m_recovery, &SignalingFailable::errorOccured, this, &FlipperZero::onControllerErrorOccured); + } else { m_remote = new RemoteController(m_deviceInfo.serialInfo, this); + m_storage = new StorageController(m_deviceInfo.serialInfo, this); + + connect(m_storage, &SignalingFailable::errorOccured, this, &FlipperZero::onControllerErrorOccured); } } diff --git a/backend/flipperzero/flipperzero.h b/backend/flipperzero/flipperzero.h index 375c42a6..2b76f59f 100644 --- a/backend/flipperzero/flipperzero.h +++ b/backend/flipperzero/flipperzero.h @@ -9,6 +9,7 @@ namespace Flipper { namespace Zero { class RemoteController; class RecoveryController; + class StorageController; } class FlipperZero : public QObject @@ -64,6 +65,7 @@ class FlipperZero : public QObject Flipper::Zero::RemoteController *remote() const; Flipper::Zero::RecoveryController *recovery() const; + Flipper::Zero::StorageController *storage() const; void setName(const QString &name); void setTarget(const QString &target); @@ -81,6 +83,9 @@ class FlipperZero : public QObject void isOnlineChanged(); void isErrorChanged(); +private slots: + void onControllerErrorOccured(); + private: void initControllers(); @@ -97,6 +102,7 @@ class FlipperZero : public QObject Zero::RemoteController *m_remote; Zero::RecoveryController *m_recovery; + Zero::StorageController *m_storage; }; } diff --git a/backend/flipperzero/operations/assetsdownloadoperation.cpp b/backend/flipperzero/operations/assetsdownloadoperation.cpp new file mode 100644 index 00000000..72fc456d --- /dev/null +++ b/backend/flipperzero/operations/assetsdownloadoperation.cpp @@ -0,0 +1,379 @@ +#include "assetsdownloadoperation.h" + +#include +#include +#include +#include + +#include "flipperzero/storage/removeoperation.h" +#include "flipperzero/storage/mkdiroperation.h" +#include "flipperzero/storage/writeoperation.h" +#include "flipperzero/storage/readoperation.h" +#include "flipperzero/storage/statoperation.h" +#include "flipperzero/storagecontroller.h" +#include "flipperzero/assetmanifest.h" +#include "flipperzero/flipperzero.h" +#include "gzipuncompressor.h" +#include "tararchive.h" + +#include "macros.h" + +#define RESOURCES_PREFIX QByteArrayLiteral("resources") +#define DEVICE_MANIFEST QByteArrayLiteral("/ext/Manifest") + +using namespace Flipper; +using namespace Zero; + +static void print_file_list(const QString &header, const FileNode::FileInfoList &list) +{ + qDebug() << header; + + for(const auto &e : list) { + const auto icon = QStringLiteral("[%1]").arg(e.type == FileNode::Type::RegularFile ? 'F' : + e.type == FileNode::Type::Directory ? 'D' : '?'); + qDebug() << icon << e.absolutePath; + } +} + +AssetsDownloadOperation::AssetsDownloadOperation(FlipperZero *device, QIODevice *file, QObject *parent): + Operation(device, parent), + m_compressed(file), + m_uncompressed(nullptr), + m_isDeviceManifestPresent(false) +{ + device->setMessage(QStringLiteral("Databases download pending...")); +} + +AssetsDownloadOperation::~AssetsDownloadOperation() +{} + +const QString AssetsDownloadOperation::description() const +{ + return QStringLiteral("Assets Download @%1 %2").arg(device()->model(), device()->name()); +} + +void AssetsDownloadOperation::transitionToNextState() +{ + if(state() == BasicState::Ready) { + setState(State::CheckingExtStorage); + if(!checkForExtStorage()) { + finishWithError(QStringLiteral("Failed to access the external storage")); + } + + } else if(state() == State::CheckingExtStorage) { + setState(State::ExtractingArchive); + if(!extractArchive()) { + finishWithError(QStringLiteral("Failed to extract the databases archive")); + } + + } else if(state() == State::ExtractingArchive) { + setState(ReadingLocalManifest); + if(!readLocalManifest()) { + finishWithError(QStringLiteral("Failed to read local manifest")); + } + + } else if(state() == State::ReadingLocalManifest) { + setState(CheckingDeviceManifest); + if(!checkForDeviceManifest()) { + finishWithError(QStringLiteral("Failed to check for device manifest")); + } + + } else if(state() == State::CheckingDeviceManifest) { + setState(ReadingDeviceManifest); + if(!readDeviceManifest()) { + finishWithError(QStringLiteral("Failed to read device manifest")); + } + + } else if(state() == State::ReadingDeviceManifest) { + setState(State::BuildingFileLists); + if(!buildFileLists()) { + finishWithError(QStringLiteral("Failed to build file lists")); + } + + } else if(state() == State::BuildingFileLists) { + setState(State::DeletingFiles); + if(!deleteFiles()) { + finishWithError(QStringLiteral("Failed to delete files")); + } + + } else if(state() == State::DeletingFiles) { + setState(State::WritingFiles); + if(!writeFiles()) { + finishWithError(QStringLiteral("Failed to write files")); + } + + } else if(state() == State::WritingFiles) { + finish(); + } + + if(isError()) { + device()->setError(errorString()); + } +} + +void AssetsDownloadOperation::onOperationTimeout() +{ + qDebug() << "Operation timeout"; +} + +bool AssetsDownloadOperation::checkForExtStorage() +{ + auto *op = device()->storage()->stat(QByteArrayLiteral("/ext")); + + connect(op, &AbstractOperation::finished, this, [=]() { + if(op->isError()) { + finishWithError("Failed to perform stat operation"); + } else if(op->type() == StatOperation::Type::InternalError) { + info_msg("No external storage found, finishing early."); + finish(); + + } else if(op->type() != StatOperation::Type::Storage) { + finishWithError("/ext is not a storage"); + } else { + info_msg(QStringLiteral("External storage is present, %1 bytes free.").arg(op->sizeFree())); + transitionToNextState(); + } + + if(isError()) { + device()->setError(errorString()); + } + + op->deleteLater(); + }); + + return true; +} + +bool AssetsDownloadOperation::extractArchive() +{ + // TODO: put everything into a directory & generate random names for the files + const auto tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + check_return_bool(!tempPath.isEmpty(), "Failed to find a suitable temporary location."); + + check_return_bool(m_compressed->open(QIODevice::ReadOnly), m_compressed->errorString()); + + // TODO: check if file exists, etc. + m_uncompressed = new QFile(tempPath + "/qflipper-databases.tar", this); + check_return_bool(m_uncompressed->open(QIODevice::ReadWrite), m_uncompressed->errorString()); + + auto *uncompressor = new GZipUncompressor(m_compressed, m_uncompressed, this); + + connect(uncompressor, &GZipUncompressor::finished, this, [=]() { + if(uncompressor->isError()) { + finishWithError(uncompressor->errorString()); + } else { + m_archive = TarArchive(m_uncompressed); + + if(m_archive.isError()) { + finishWithError(m_archive.errorString()); + } else { + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + } + } + + uncompressor->deleteLater(); + m_compressed->deleteLater(); + }); + + return true; +} + +bool AssetsDownloadOperation::readLocalManifest() +{ + const auto text = m_archive.fileData(QStringLiteral("resources/Manifest")); + check_return_bool(!text.isEmpty(), m_archive.errorString()); + + m_localManifest = AssetManifest(text); + check_return_bool(!m_localManifest.isError(), m_localManifest.errorString()); + + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + return true; +} + +bool AssetsDownloadOperation::checkForDeviceManifest() +{ + auto *op = device()->storage()->stat(DEVICE_MANIFEST); + connect(op, &AbstractOperation::finished, this, [=]() { + if(op->isError()) { + finishWithError(op->errorString()); + } else { + if(op->type() != StatOperation::Type::RegularFile) { + setState(State::ReadingDeviceManifest); + } else { + m_isDeviceManifestPresent = true; + } + + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + } + + op->deleteLater(); + }); + + return true; +} + +bool AssetsDownloadOperation::readDeviceManifest() +{ + auto *buf = new QBuffer(this); + + if(!buf->open(QIODevice::ReadWrite)) { + buf->deleteLater(); + return false; + } + + auto *op = device()->storage()->read(QByteArrayLiteral("/ext/Manifest"), buf); + + connect(op, &AbstractOperation::finished, this, [=]() { + if(!op->isError()) { + m_deviceManifest = AssetManifest(buf->readAll()); + } + + op->deleteLater(); + buf->deleteLater(); + + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + }); + + return true; +} + +bool AssetsDownloadOperation::buildFileLists() +{ + FileNode::FileInfoList deleted, added, changed; + + FileNode::FileInfo manifestInfo; + manifestInfo.name = QStringLiteral("Manifest"); + manifestInfo.absolutePath = manifestInfo.name; + manifestInfo.type = FileNode::Type::RegularFile; + + print_file_list("<<<<< Local manifest:", m_localManifest.tree()->toPreOrderList()); + + if(!m_isDeviceManifestPresent) { + info_msg("Device manifest not present, assumimg fresh install..."); + added.append(manifestInfo); + added.append(m_localManifest.tree()->toPreOrderList().mid(1)); + + } else if(m_deviceManifest.isError()) { + info_msg("Failed to build device manifest, assuming complete asset overwrite..."); + changed.append(manifestInfo); + changed.append(m_localManifest.tree()->toPreOrderList().mid(1)); + + } else { + print_file_list(">>>>> Device manifest:", m_deviceManifest.tree()->toPreOrderList()); + + deleted.append(m_localManifest.tree()->difference(m_deviceManifest.tree())); + added.append(m_deviceManifest.tree()->difference(m_localManifest.tree())); + changed.append(m_deviceManifest.tree()->changed(m_localManifest.tree())); + + if(!deleted.isEmpty() || !added.isEmpty() || !changed.isEmpty()) { + changed.prepend(manifestInfo); + } + } + + if(!deleted.isEmpty()) { + print_file_list("----- Files deleted:", deleted); + } + + if(!added.isEmpty()) { + print_file_list("+++++ Files added:", added); + } + + if(!changed.isEmpty()) { + print_file_list("***** Files changed:", changed); + } + + m_delete.append(deleted); + m_delete.append(changed); + + m_write.append(added); + m_write.append(changed); + + // Start deleting by the farthest nodes + std::reverse(m_delete.begin(), m_delete.end()); + + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + return true; +} + +bool AssetsDownloadOperation::deleteFiles() +{ + if(m_delete.isEmpty()) { + info_msg("No files to delete, skipping to write"); + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + return true; + } + + device()->setMessage(tr("Deleting unneeded files...")); + + int i = m_delete.size(); + + for(const auto &fileInfo : qAsConst(m_delete)) { + --i; + const auto fileName = QByteArrayLiteral("/ext/") + fileInfo.absolutePath.toLocal8Bit(); + auto *op = device()->storage()->remove(fileName); + connect(op, &AbstractOperation::finished, this, [=]() { + if(op->isError()) { + finishWithError(op->errorString()); + } else if(i == 0) { + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + } + }); + } + + return true; +} + +bool AssetsDownloadOperation::writeFiles() +{ + if(m_write.isEmpty()) { + info_msg("No files to write, skipping to the end"); + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + return true; + } + + + device()->setMessage(tr("Writing new files...")); + + int i = m_write.size(); + + for(const auto &fileInfo : qAsConst(m_write)) { + --i; + + AbstractOperation *op; + const auto filePath = QByteArrayLiteral("/ext/") + fileInfo.absolutePath.toLocal8Bit(); + + if(fileInfo.type == FileNode::Type::Directory) { + op = device()->storage()->mkdir(filePath); + + } else if(fileInfo.type == FileNode::Type::RegularFile) { + auto *buf = new QBuffer(this); + if(!buf->open(QIODevice::ReadWrite)) { + buf->deleteLater(); + return false; + } + + const auto resourcePath = QStringLiteral("resources/") + fileInfo.absolutePath; + if((buf->write(m_archive.fileData(resourcePath)) <= 0) || (!buf->seek(0))) { + buf->deleteLater(); + return false; + } + + op = device()->storage()->write(filePath, buf); + + } else { + return false; + } + + connect(op, &AbstractOperation::finished, this, [=]() { + if(op->isError()) { + finishWithError(op->errorString()); + } else if(i == 0) { + QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); + } + + op->deleteLater(); + }); + } + + return true; +} diff --git a/backend/flipperzero/operations/assetsdownloadoperation.h b/backend/flipperzero/operations/assetsdownloadoperation.h new file mode 100644 index 00000000..c2ac5269 --- /dev/null +++ b/backend/flipperzero/operations/assetsdownloadoperation.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include "tararchive.h" +#include "flipperzerooperation.h" +#include "flipperzero/assetmanifest.h" + +class QIODevice; + +namespace Flipper { +namespace Zero { + +class AssetsDownloadOperation : public Operation +{ + Q_OBJECT + +public: + enum State { + CheckingExtStorage = BasicState::User, + ExtractingArchive, + ReadingLocalManifest, + CheckingDeviceManifest, + ReadingDeviceManifest, + BuildingFileLists, + DeletingFiles, + WritingFiles + }; + + AssetsDownloadOperation(FlipperZero *device, QIODevice *file, QObject *parent = nullptr); + ~AssetsDownloadOperation(); + + const QString description() const override; + +private slots: + void transitionToNextState() override; + void onOperationTimeout() override; + +private: + bool checkForExtStorage(); + bool extractArchive(); + bool readLocalManifest(); + bool checkForDeviceManifest(); + bool readDeviceManifest(); + + bool buildFileLists(); + + bool deleteFiles(); + bool writeFiles(); + + QIODevice *m_compressed; + QIODevice *m_uncompressed; + + TarArchive m_archive; + + AssetManifest m_localManifest; + AssetManifest m_deviceManifest; + bool m_isDeviceManifestPresent; + + FileNode::FileInfoList m_delete; + FileNode::FileInfoList m_write; +}; + +} +} + diff --git a/backend/flipperzero/operations/firmwaredownloadoperation.cpp b/backend/flipperzero/operations/firmwaredownloadoperation.cpp index 81ff26ee..05f6d5c9 100644 --- a/backend/flipperzero/operations/firmwaredownloadoperation.cpp +++ b/backend/flipperzero/operations/firmwaredownloadoperation.cpp @@ -21,7 +21,7 @@ FirmwareDownloadOperation::~FirmwareDownloadOperation() m_file->deleteLater(); } -const QString FirmwareDownloadOperation::name() const +const QString FirmwareDownloadOperation::description() const { return QStringLiteral("Firmware Download @%1 %2").arg(device()->model(), device()->name()); } diff --git a/backend/flipperzero/operations/firmwaredownloadoperation.h b/backend/flipperzero/operations/firmwaredownloadoperation.h index 6a48eb4e..bc7daf0a 100644 --- a/backend/flipperzero/operations/firmwaredownloadoperation.h +++ b/backend/flipperzero/operations/firmwaredownloadoperation.h @@ -24,7 +24,7 @@ class FirmwareDownloadOperation : public Operation FirmwareDownloadOperation(FlipperZero *device, QIODevice *file, QObject *parent = nullptr); ~FirmwareDownloadOperation(); - const QString name() const override; + const QString description() const override; private slots: void transitionToNextState() override; diff --git a/backend/flipperzero/operations/fixbootissuesoperation.cpp b/backend/flipperzero/operations/fixbootissuesoperation.cpp index d96cccb2..25020231 100644 --- a/backend/flipperzero/operations/fixbootissuesoperation.cpp +++ b/backend/flipperzero/operations/fixbootissuesoperation.cpp @@ -18,7 +18,7 @@ FixBootIssuesOperation::~FixBootIssuesOperation() device()->setPersistent(false); } -const QString FixBootIssuesOperation::name() const +const QString FixBootIssuesOperation::description() const { return QStringLiteral("Fix boot issues @%1 %2").arg(device()->model(), device()->name()); } diff --git a/backend/flipperzero/operations/fixbootissuesoperation.h b/backend/flipperzero/operations/fixbootissuesoperation.h index 46e07d2c..a31feecd 100644 --- a/backend/flipperzero/operations/fixbootissuesoperation.h +++ b/backend/flipperzero/operations/fixbootissuesoperation.h @@ -21,7 +21,7 @@ class FixBootIssuesOperation : public Operation FixBootIssuesOperation(FlipperZero *device, QObject *parent = nullptr); ~FixBootIssuesOperation(); - const QString name() const override; + const QString description() const override; private slots: void transitionToNextState() override; diff --git a/backend/flipperzero/operations/fixoptionbytesoperation.cpp b/backend/flipperzero/operations/fixoptionbytesoperation.cpp index 059a4f16..2026a4f9 100644 --- a/backend/flipperzero/operations/fixoptionbytesoperation.cpp +++ b/backend/flipperzero/operations/fixoptionbytesoperation.cpp @@ -20,7 +20,7 @@ FixOptionBytesOperation::~FixOptionBytesOperation() m_file->deleteLater(); } -const QString FixOptionBytesOperation::name() const +const QString FixOptionBytesOperation::description() const { return QStringLiteral("Fix Option Bytes @%1 %2").arg(device()->model(), device()->name()); } diff --git a/backend/flipperzero/operations/fixoptionbytesoperation.h b/backend/flipperzero/operations/fixoptionbytesoperation.h index 650793cc..6094853b 100644 --- a/backend/flipperzero/operations/fixoptionbytesoperation.h +++ b/backend/flipperzero/operations/fixoptionbytesoperation.h @@ -23,7 +23,7 @@ class FixOptionBytesOperation : public Operation FixOptionBytesOperation(FlipperZero *device, QIODevice *file, QObject *parent = nullptr); ~FixOptionBytesOperation(); - const QString name() const override; + const QString description() const override; private slots: void transitionToNextState() override; diff --git a/backend/flipperzero/operations/wirelessstackdownloadoperation.cpp b/backend/flipperzero/operations/wirelessstackdownloadoperation.cpp index e210bbc7..697eb906 100644 --- a/backend/flipperzero/operations/wirelessstackdownloadoperation.cpp +++ b/backend/flipperzero/operations/wirelessstackdownloadoperation.cpp @@ -27,7 +27,7 @@ WirelessStackDownloadOperation::~WirelessStackDownloadOperation() m_file->deleteLater(); } -const QString WirelessStackDownloadOperation::name() const +const QString WirelessStackDownloadOperation::description() const { return QStringLiteral("Co-Processor Firmware Download @%1 %2").arg(device()->model(), device()->name()); } diff --git a/backend/flipperzero/operations/wirelessstackdownloadoperation.h b/backend/flipperzero/operations/wirelessstackdownloadoperation.h index 302f00b1..b2253e85 100644 --- a/backend/flipperzero/operations/wirelessstackdownloadoperation.h +++ b/backend/flipperzero/operations/wirelessstackdownloadoperation.h @@ -29,7 +29,7 @@ class WirelessStackDownloadOperation : public Operation WirelessStackDownloadOperation(FlipperZero *device, QIODevice *file, uint32_t targetAddress = 0, QObject *parent = nullptr); ~WirelessStackDownloadOperation(); - const QString name() const override; + const QString description() const override; private slots: void transitionToNextState() override; diff --git a/backend/flipperzero/recoverycontroller.cpp b/backend/flipperzero/recoverycontroller.cpp index 59e82511..ee1edacc 100644 --- a/backend/flipperzero/recoverycontroller.cpp +++ b/backend/flipperzero/recoverycontroller.cpp @@ -19,7 +19,7 @@ using namespace WB55; #define to_hex_str(num) (QString::number(num, 16)) RecoveryController::RecoveryController(USBDeviceInfo info, QObject *parent): - QObject(parent), + SignalingFailable(parent), m_usbInfo(info) {} @@ -374,11 +374,6 @@ const QString &RecoveryController::message() const return m_message; } -const QString &RecoveryController::errorString() const -{ - return m_errorString; -} - double RecoveryController::progress() const { return m_progress; @@ -390,11 +385,3 @@ void RecoveryController::setMessage(const QString &msg) emit messageChanged(); } - -void RecoveryController::setError(const QString &msg) -{ - m_errorString = msg; - m_isError = true; - - emit errorOccured(); -} diff --git a/backend/flipperzero/recoverycontroller.h b/backend/flipperzero/recoverycontroller.h index 208a08ff..956101f3 100644 --- a/backend/flipperzero/recoverycontroller.h +++ b/backend/flipperzero/recoverycontroller.h @@ -3,13 +3,14 @@ #include #include "usbdeviceinfo.h" +#include "signalingfailable.h" class QIODevice; namespace Flipper { namespace Zero { -class RecoveryController : public QObject +class RecoveryController : public SignalingFailable { Q_OBJECT @@ -31,11 +32,9 @@ class RecoveryController : public QObject ~RecoveryController(); const QString &message() const; - const QString &errorString() const; WirelessStatus wirelessStatus(); double progress() const; - bool isError() const; bool leaveDFU(); bool setBootMode(BootMode mode); @@ -51,19 +50,15 @@ class RecoveryController : public QObject signals: void messageChanged(); - void errorOccured(); void progressChanged(); private: void setProgress(double progress); void setMessage(const QString &msg); - void setError(const QString &msg); USBDeviceInfo m_usbInfo; - bool m_isError; QString m_message; - QString m_errorString; double m_progress; }; diff --git a/backend/flipperzero/remotecontroller.cpp b/backend/flipperzero/remotecontroller.cpp index c9ae1654..54221813 100644 --- a/backend/flipperzero/remotecontroller.cpp +++ b/backend/flipperzero/remotecontroller.cpp @@ -65,7 +65,7 @@ void RemoteController::sendInputEvent(InputKey key, InputType type) void RemoteController::onPortReadyRead() { - static const auto header = QByteArray::fromHex("F0E1D2C3"); + static const auto header = QByteArrayLiteral("\xf0\xe1\xd2\xc3"); m_dataBuffer += m_port->readAll(); @@ -123,8 +123,7 @@ void RemoteController::closePort() disconnect(m_port, &QSerialPort::readyRead, this, &RemoteController::onPortReadyRead); disconnect(m_port, &QSerialPort::errorOccurred, this, &RemoteController::onPortErrorOccured); - m_port->write("\x01\r"); - m_port->flush(); + m_port->write("\x01\r\n"); m_port->clear(); m_port->close(); } diff --git a/backend/flipperzero/storage/mkdiroperation.cpp b/backend/flipperzero/storage/mkdiroperation.cpp new file mode 100644 index 00000000..183e07e0 --- /dev/null +++ b/backend/flipperzero/storage/mkdiroperation.cpp @@ -0,0 +1,49 @@ +#include "mkdiroperation.h" +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +MkDirOperation::MkDirOperation(QSerialPort *serialPort, const QByteArray &dirName, QObject *parent): + SimpleSerialOperation(serialPort, parent), + m_dirName(dirName) +{} + +const QString MkDirOperation::description() const +{ + return QStringLiteral("MkDir @%1").arg(QString(m_dirName)); +} + +QByteArray MkDirOperation::endOfMessageToken() const +{ + return QByteArrayLiteral("\r\n\r\n>: \a"); +} + +QByteArray MkDirOperation::commandLine() const +{ + return QByteArrayLiteral("storage mkdir ") + m_dirName + QByteArrayLiteral("\r\n"); +} + +bool MkDirOperation::parseReceivedData() +{ + const auto lines = receivedData().split('\n'); + + if(lines.size() == 4) { + const auto msg = lines.at(1).trimmed(); + + if(!msg.startsWith(QByteArrayLiteral("Storage error: "))) { + return false; + } else if(!msg.endsWith(QByteArrayLiteral("file/dir already exist"))) { + setError(msg); + } else { + info_msg(QStringLiteral("Warning: directory %1 already exists.").arg(QString(m_dirName))); + } + + return true; + + } else if(lines.size() == 3) { + return true; + } else { + return false; + } +} diff --git a/backend/flipperzero/storage/mkdiroperation.h b/backend/flipperzero/storage/mkdiroperation.h new file mode 100644 index 00000000..a2b060b1 --- /dev/null +++ b/backend/flipperzero/storage/mkdiroperation.h @@ -0,0 +1,27 @@ +#pragma once + +#include "simpleserialoperation.h" + +namespace Flipper { +namespace Zero { + +class MkDirOperation : public SimpleSerialOperation +{ + Q_OBJECT + +public: + + MkDirOperation(QSerialPort *serialPort, const QByteArray &dirName, QObject *parent = nullptr); + const QString description() const override; + +private: + QByteArray endOfMessageToken() const override; + QByteArray commandLine() const override; + bool parseReceivedData() override; + + QByteArray m_dirName; +}; + +} +} + diff --git a/backend/flipperzero/storage/readoperation.cpp b/backend/flipperzero/storage/readoperation.cpp new file mode 100644 index 00000000..7047288e --- /dev/null +++ b/backend/flipperzero/storage/readoperation.cpp @@ -0,0 +1,127 @@ +#include "readoperation.h" + +#include + +#define READY_PROMPT QByteArrayLiteral("\r\nReady?\r\n") +#define FINISH_PROMPT QByteArrayLiteral("\r\n\r\n>: ") + +#define READY_PROMPT_LINE_COUNT 5 +#define FINISH_PROMPT_LINE_COUNT 4 + +#define CHUNK_SIZE 512 + +using namespace Flipper; +using namespace Zero; + +ReadOperation::ReadOperation(QSerialPort *serialPort, const QByteArray &fileName, QIODevice *file, QObject *parent): + AbstractSerialOperation(serialPort, parent), + m_size(0), + m_fileName(fileName), + m_file(file) +{} + +const QString ReadOperation::description() const +{ + return QStringLiteral("Read @%1").arg(QString(m_fileName)); +} + +void ReadOperation::onSerialPortReadyRead() +{ + startTimeout(); + + m_receivedData += serialPort()->readAll(); + + if(state() == State::SettingUp) { + if(m_receivedData.endsWith(READY_PROMPT)) { + if(!parseSetupReply()) { + finish(); + } else { + setState(State::ReceivingData); + serialPort()->write("\n"); + } + + m_receivedData.clear(); + } else if(m_receivedData.endsWith(FINISH_PROMPT)) { + parseError(); + finish(); + } + + } else if(state() == State::ReceivingData) { + if(m_receivedData.endsWith(READY_PROMPT)) { + m_file->write(m_receivedData.chopped(READY_PROMPT.size())); + m_receivedData.clear(); + serialPort()->write("\n"); + + } else if(m_receivedData.endsWith(FINISH_PROMPT)) { + m_file->write(m_receivedData.chopped(FINISH_PROMPT.size())); + m_file->seek(0); + finish(); + } + } +} + +bool ReadOperation::begin() +{ + const auto cmdLine = QByteArrayLiteral("storage read_chunks ") + m_fileName + QByteArrayLiteral(" ") + + QByteArray::number(CHUNK_SIZE, 10) + QByteArrayLiteral("\r"); + const auto success = (serialPort()->write(cmdLine) == cmdLine.size()) && serialPort()->flush(); + + if(success) { + setState(State::SettingUp); + startTimeout(); + } + + return success; +} + +bool ReadOperation::parseError() +{ + const auto lines = m_receivedData.split('\n'); + + if(lines.size() != FINISH_PROMPT_LINE_COUNT) { + setError(QStringLiteral("Unexpected error message line count")); + return false; + } + + const auto &msg = lines.at(1).trimmed(); + + if(!msg.startsWith("Storage error:")) { + setError(QStringLiteral("Unexpected error message format")); + return false; + } + + setError(msg); + return true; +} + +bool ReadOperation::parseSetupReply() +{ + const auto lines = m_receivedData.split('\n'); + if(lines.size() != READY_PROMPT_LINE_COUNT) { + setError(QStringLiteral("Unexpected setup message line count")); + return false; + } + + const auto &sizeMsg = lines.at(1); + if(!sizeMsg.startsWith("Size:")) { + setError(QStringLiteral("Unexpected setup message format")); + return false; + } + + return parseSize(sizeMsg); +} + +bool ReadOperation::parseSize(const QByteArray &s) +{ + const auto tokens = s.split(':'); + + if(tokens.size() != 2) { + setError(QStringLiteral("Unexpected size message format")); + return false; + } + + bool success; + m_size = tokens.at(1).toLongLong(&success, 10); + + return success; +} diff --git a/backend/flipperzero/storage/readoperation.h b/backend/flipperzero/storage/readoperation.h new file mode 100644 index 00000000..eac2b449 --- /dev/null +++ b/backend/flipperzero/storage/readoperation.h @@ -0,0 +1,43 @@ +#pragma once + +#include "abstractserialoperation.h" + +#include + +class QIODevice; + +namespace Flipper { +namespace Zero { + +class ReadOperation : public AbstractSerialOperation +{ + Q_OBJECT + + enum State { + SettingUp = BasicState::Ready, + ReceivingData + }; + +public: + ReadOperation(QSerialPort *serialPort, const QByteArray &fileName, QIODevice *file, QObject *parent = nullptr); + const QString description() const override; + +private slots: + void onSerialPortReadyRead() override; + +private: + bool begin() override; + + bool parseError(); + bool parseSetupReply(); + bool parseSize(const QByteArray &s); + + qint64 m_size; + QByteArray m_fileName; + QIODevice *m_file; + QByteArray m_receivedData; +}; + +} +} + diff --git a/backend/flipperzero/storage/removeoperation.cpp b/backend/flipperzero/storage/removeoperation.cpp new file mode 100644 index 00000000..d1b99b83 --- /dev/null +++ b/backend/flipperzero/storage/removeoperation.cpp @@ -0,0 +1,50 @@ +#include "removeoperation.h" + +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +RemoveOperation::RemoveOperation(QSerialPort *serialPort, const QByteArray &fileName, QObject *parent): + SimpleSerialOperation(serialPort, parent), + m_fileName(fileName) +{} + +const QString RemoveOperation::description() const +{ + return QStringLiteral("Remove @%1").arg(QString(m_fileName)); +} + +QByteArray RemoveOperation::endOfMessageToken() const +{ + return QByteArrayLiteral("\r\n\r\n>: \a"); +} + +QByteArray RemoveOperation::commandLine() const +{ + return QByteArrayLiteral("storage remove ") + m_fileName + QByteArrayLiteral("\r\n"); +} + +bool RemoveOperation::parseReceivedData() +{ + const auto lines = receivedData().split('\n'); + + if(lines.size() == 4) { + const auto msg = lines.at(1).trimmed(); + + if(!msg.startsWith(QByteArrayLiteral("Storage error: "))) { + return false; + } else if(!msg.endsWith(QByteArrayLiteral("file/dir not exist"))) { + setError(msg); + } else { + info_msg(QStringLiteral("Warning: file %1 does not exist.").arg(QString(m_fileName))); + } + + return true; + + } else if(lines.size() == 3) { + return true; + } else { + return false; + } +} diff --git a/backend/flipperzero/storage/removeoperation.h b/backend/flipperzero/storage/removeoperation.h new file mode 100644 index 00000000..1548d2ce --- /dev/null +++ b/backend/flipperzero/storage/removeoperation.h @@ -0,0 +1,27 @@ +#pragma once + +#include "simpleserialoperation.h" + +namespace Flipper { +namespace Zero { + +class RemoveOperation : public SimpleSerialOperation +{ + Q_OBJECT + +public: + RemoveOperation(QSerialPort *serialPort, const QByteArray &fileName, QObject *parent = nullptr); + const QString description() const override; + +private: + QByteArray endOfMessageToken() const override; + QByteArray commandLine() const override; + + bool parseReceivedData() override; + + QByteArray m_fileName; +}; + +} +} + diff --git a/backend/flipperzero/storage/statoperation.cpp b/backend/flipperzero/storage/statoperation.cpp new file mode 100644 index 00000000..7df6a9b6 --- /dev/null +++ b/backend/flipperzero/storage/statoperation.cpp @@ -0,0 +1,132 @@ +#include "statoperation.h" + +#include +#include + +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +static qint64 fromStringSize(const QByteArray &s); + +StatOperation::StatOperation(QSerialPort *serialPort, const QByteArray &fileName, QObject *parent): + SimpleSerialOperation(serialPort, parent), + m_fileName(fileName), + m_size(-1), + m_sizeFree(-1), + m_type(Type::Invalid) +{} + +const QString StatOperation::description() const +{ + return QStringLiteral("Stat @%1").arg(QString(m_fileName)); +} + +const QByteArray &StatOperation::fileName() const +{ + return m_fileName; +} + +qint64 StatOperation::size() const +{ + return m_size; +} + +qint64 StatOperation::sizeFree() const +{ + return m_sizeFree; +} + +StatOperation::Type StatOperation::type() const +{ + return m_type; +} + +QByteArray StatOperation::endOfMessageToken() const +{ + return QByteArrayLiteral("\r\n\r\n>: \a"); +} + +QByteArray StatOperation::commandLine() const +{ + return QByteArrayLiteral("storage stat ") + m_fileName + QByteArrayLiteral("\r\n"); +} + +bool StatOperation::parseReceivedData() +{ + const auto lines = receivedData().split('\n'); + check_return_bool(lines.size() == 4, "Wrong reply line count."); + + const auto reply = lines[1].trimmed().toLower(); + + if(reply == "storage error: file/dir not exist") { + m_type = Type::NotFound; + + } else if(reply == "storage error: invalid name/path") { + m_type = Type::Invalid; + + } else if(reply == "storage error: internal error") { + m_type = Type::InternalError; + + } else if(parseFileSize(reply)) { + m_type = Type::RegularFile; + + } else if(reply.contains("directory")) { + m_type = Type::Directory; + + } else if(parseStorageSize(reply)) { + m_type = Type::Storage; + + } else { + error_msg("Unexpected stat reply string."); + return false; + } + + return true; +} + +bool StatOperation::parseFileSize(const QByteArray &data) +{ + QRegExp expr("file, size: ([0-9]+k?i?b)"); + + if(!expr.exactMatch(data) || (expr.captureCount() != 1)) { + return false; + } + + const auto captures = expr.capturedTexts(); + m_size = fromStringSize(captures[1].toLocal8Bit()); + + return m_size >= 0; +} + +bool StatOperation::parseStorageSize(const QByteArray &data) +{ + QRegExp expr("storage, ([0-9]+k?i?b) total, ([0-9]+k?i?b) free"); + + if(!expr.exactMatch(data) || (expr.captureCount() != 2)) { + return false; + } + + const auto captures = expr.capturedTexts(); + m_size = fromStringSize(captures[1].toLocal8Bit()); + m_sizeFree = fromStringSize(captures[2].toLocal8Bit()); + + return (m_size >= 0) && (m_sizeFree >= 0); +} + +static qint64 fromStringSize(const QByteArray &s) +{ + qint64 result = -1; + bool cr = true; + + if(s.endsWith("kb")) { + result = s.chopped(2).toULongLong(&cr, 10) * 1024; //TODO: fix incorrect multipliers in the firmware? + } else if(s.endsWith("kib")) { + result = s.chopped(3).toULongLong(&cr, 10) * 1024; + } else if(s.endsWith('b')) { + result = s.chopped(1).toULongLong(&cr, 10); + } + + return cr ? result : -1; +} diff --git a/backend/flipperzero/storage/statoperation.h b/backend/flipperzero/storage/statoperation.h new file mode 100644 index 00000000..872df51b --- /dev/null +++ b/backend/flipperzero/storage/statoperation.h @@ -0,0 +1,49 @@ +#pragma once + +#include "simpleserialoperation.h" + +namespace Flipper { +namespace Zero { + +class StatOperation : public SimpleSerialOperation +{ + Q_OBJECT + +public: + enum class Type { + RegularFile, + Directory, + Storage, + NotFound, + InternalError, + Invalid + }; + + Q_ENUM(Type) + + StatOperation(QSerialPort *serialPort, const QByteArray &fileName, QObject *parent = nullptr); + + const QString description() const override; + + const QByteArray &fileName() const; + qint64 size() const; + qint64 sizeFree() const; + Type type() const; + +private: + QByteArray endOfMessageToken() const override; + QByteArray commandLine() const override; + bool parseReceivedData() override; + + bool parseFileSize(const QByteArray &data); + bool parseStorageSize(const QByteArray &data); + + QByteArray m_fileName; + qint64 m_size; + qint64 m_sizeFree; + Type m_type; +}; + +} +} + diff --git a/backend/flipperzero/storage/writeoperation.cpp b/backend/flipperzero/storage/writeoperation.cpp new file mode 100644 index 00000000..beac7e23 --- /dev/null +++ b/backend/flipperzero/storage/writeoperation.cpp @@ -0,0 +1,122 @@ +#include "writeoperation.h" + +#include +#include + +#include "macros.h" + +#define READY_PROMPT QByteArrayLiteral("\r\nReady\r\n") +#define FINISH_PROMPT QByteArrayLiteral("\r\n>: ") + +#define FINISH_PROMPT_LINE_COUNT 4 + +#define CHUNK_SIZE 512 + +using namespace Flipper; +using namespace Zero; + +WriteOperation::WriteOperation(QSerialPort *serialPort, const QByteArray &fileName, QIODevice *file, QObject *parent): + AbstractSerialOperation(serialPort, parent), + m_fileName(fileName), + m_file(file) +{} + +const QString WriteOperation::description() const +{ + return QStringLiteral("Write @%1").arg(QString(m_fileName)); +} + +void WriteOperation::onSerialPortReadyRead() +{ + m_receivedData.append(serialPort()->readAll()); + + if(state() == State::SettingUp) { + if(m_receivedData.endsWith(FINISH_PROMPT)) { + parseError(); + finish(); + + } else if(!m_receivedData.endsWith(READY_PROMPT)) { + return; + } + + setState(State::WritingData); + + if(!writeChunk()) { + finishWithError(QStringLiteral("Failed to write chunk")); + } + + } else if(state() == State::WritingData) { + if(!m_receivedData.endsWith(FINISH_PROMPT)) { + return; + } + + setState(State::SettingUp); + + if(!m_file->bytesAvailable()) { + finish(); + } else if(!writeSetupCommand()) { + finishWithError(QStringLiteral("Failed to write chunk")); + } + + } else { + finishWithError(QStringLiteral("Unexpected data")); + } + + m_receivedData.clear(); +} + +bool WriteOperation::begin() +{ + check_return_bool(m_file->bytesAvailable(), "No data is available for reading from file"); + + setState(State::SettingUp); + return writeSetupCommand(); +} + +bool WriteOperation::writeSetupCommand() +{ + const auto bytesAvailable = m_file->bytesAvailable(); + m_chunkSize = bytesAvailable < CHUNK_SIZE ? bytesAvailable : CHUNK_SIZE; + const auto cmdLine = QByteArrayLiteral("storage write_chunk ") + m_fileName + QByteArrayLiteral(" ") + + QByteArray::number(m_chunkSize) + QByteArrayLiteral("\r"); + + const auto success = (serialPort()->write(cmdLine) == cmdLine.size()) && serialPort()->flush(); + + if(success) { + startTimeout(); + } + + return success; +} + +bool WriteOperation::writeChunk() +{ + const auto data = m_file->read(m_chunkSize); + const auto success = (serialPort()->write(data) == data.size()) && serialPort()->flush(); + + if(success) { + startTimeout(); + } + + return success; +} + +bool WriteOperation::parseError() +{ + const auto lines = m_receivedData.split('\n'); + + if(lines.size() != FINISH_PROMPT_LINE_COUNT) { + setError(QStringLiteral("Unexpected error message line count")); + return false; + } + + const auto &msg = lines.at(1).trimmed(); + + if(!msg.startsWith("Storage error:")) { + setError(QStringLiteral("Unexpected error message format")); + return false; + } + + setError(msg); + return true; +} diff --git a/backend/flipperzero/storage/writeoperation.h b/backend/flipperzero/storage/writeoperation.h new file mode 100644 index 00000000..1d8b68c1 --- /dev/null +++ b/backend/flipperzero/storage/writeoperation.h @@ -0,0 +1,44 @@ +#pragma once + +#include "abstractserialoperation.h" + +#include + +class QIODevice; + +namespace Flipper { +namespace Zero { + +class WriteOperation : public AbstractSerialOperation +{ + Q_OBJECT + + enum State { + SettingUp = BasicState::Ready, + WritingData + }; + +public: + WriteOperation(QSerialPort *serialPort, const QByteArray &fileName, QIODevice *file, QObject *parent = nullptr); + const QString description() const override; + +private slots: + void onSerialPortReadyRead() override; + +private: + bool begin() override; + + bool writeSetupCommand(); + bool writeChunk(); + + bool parseError(); + + QByteArray m_fileName; + QIODevice *m_file; + QByteArray m_receivedData; + qint64 m_chunkSize; +}; + +} +} + diff --git a/backend/flipperzero/storagecontroller.cpp b/backend/flipperzero/storagecontroller.cpp new file mode 100644 index 00000000..a89be3e1 --- /dev/null +++ b/backend/flipperzero/storagecontroller.cpp @@ -0,0 +1,126 @@ +#include "storagecontroller.h" + +#include +#include + +#include "common/skipmotdoperation.h" +#include "storage/removeoperation.h" +#include "storage/mkdiroperation.h" +#include "storage/writeoperation.h" +#include "storage/readoperation.h" +#include "storage/statoperation.h" + +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +StorageController::StorageController(const QSerialPortInfo &portInfo, QObject *parent): + SignalingFailable(parent), + m_serialPort(new QSerialPort(portInfo, this)), + m_state(State::Idle) +{} + +StorageController::~StorageController() +{} + +StatOperation *StorageController::stat(const QByteArray &fileName) +{ + auto *op = new StatOperation(m_serialPort, fileName, this); + enqueueOperation(op); + return op; +} + +ReadOperation *StorageController::read(const QByteArray &fileName, QIODevice *file) +{ + auto *op = new ReadOperation(m_serialPort, fileName, file, this); + enqueueOperation(op); + return op; +} + +MkDirOperation *StorageController::mkdir(const QByteArray &dirName) +{ + auto *op = new MkDirOperation(m_serialPort, dirName, this); + enqueueOperation(op); + return op; +} + +WriteOperation *StorageController::write(const QByteArray &fileName, QIODevice *file) +{ + auto *op = new WriteOperation(m_serialPort, fileName, file, this); + enqueueOperation(op); + return op; +} + +RemoveOperation *StorageController::remove(const QByteArray &fileName) +{ + auto *op = new RemoveOperation(m_serialPort, fileName, this); + enqueueOperation(op); + return op; +} + +void StorageController::processQueue() +{ + if(m_operationQueue.isEmpty()) { + closePort(); + return; + } + + auto *currentOperation = m_operationQueue.dequeue(); + + connect(currentOperation, &AbstractOperation::finished, this, [=]() { + + if(currentOperation->isError()) { + clearQueue(); + processQueue(); + setError(QStringLiteral("Operation error: %1").arg(currentOperation->errorString())); + + } else { + processQueue(); + } + + currentOperation->deleteLater(); + }); + + currentOperation->start(); +} + +bool StorageController::openPort() +{ + if(!m_serialPort->open(QIODevice::ReadWrite)) { + setError(QStringLiteral("Serial port error: %1").arg(m_serialPort->errorString())); + return false; + } + + m_state = State::Running; + m_operationQueue.prepend(new SkipMOTDOperation(m_serialPort, this)); + + return true; +} + +void StorageController::closePort() +{ + m_state = State::Idle; + m_serialPort->close(); +} + +void StorageController::enqueueOperation(AbstractSerialOperation *op) +{ + m_operationQueue.enqueue(op); + + if(m_state == State::Idle) { + if(!openPort()) { + return; + } + + resetError(); + QTimer::singleShot(0, this, &StorageController::processQueue); + } +} + +void StorageController::clearQueue() +{ + while(!m_operationQueue.isEmpty()) { + m_operationQueue.dequeue()->deleteLater(); + } +} diff --git a/backend/flipperzero/storagecontroller.h b/backend/flipperzero/storagecontroller.h new file mode 100644 index 00000000..a7e519d7 --- /dev/null +++ b/backend/flipperzero/storagecontroller.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#include "signalingfailable.h" + +class QIODevice; +class QSerialPort; + +class AbstractSerialOperation; + +namespace Flipper { +namespace Zero { + +class StatOperation; +class ReadOperation; +class MkDirOperation; +class WriteOperation; +class RemoveOperation; + +class StorageController : public SignalingFailable +{ + Q_OBJECT + + using OperationQueue = QQueue; + + enum class State { + Idle, + Running + }; + +public: + StorageController(const QSerialPortInfo &portInfo, QObject *parent = nullptr); + ~StorageController(); + + StatOperation *stat(const QByteArray &fileName); + ReadOperation *read(const QByteArray &fileName, QIODevice *file); + MkDirOperation *mkdir(const QByteArray &dirName); + WriteOperation *write(const QByteArray &fileName, QIODevice *file); + RemoveOperation *remove(const QByteArray &fileName); + +private slots: + void processQueue(); + +private: + bool openPort(); + void closePort(); + + void enqueueOperation(AbstractSerialOperation *op); + void clearQueue(); + + OperationQueue m_operationQueue; + QSerialPort *m_serialPort; + State m_state; +}; + +} +} diff --git a/backend/gzipuncompressor.cpp b/backend/gzipuncompressor.cpp new file mode 100644 index 00000000..bdc6c8f6 --- /dev/null +++ b/backend/gzipuncompressor.cpp @@ -0,0 +1,105 @@ +#include "gzipuncompressor.h" + +#include +#include +#include + +#include + +#include "macros.h" + +#define CHUNK_SIZE 1024 + +GZipUncompressor::GZipUncompressor(QIODevice *in, QIODevice *out, QObject *parent): + QObject(parent), + m_in(in), + m_out(out), + m_progress(0) +{ + auto *watcher = new QFutureWatcher(this); + + connect(watcher, &QFutureWatcherBase::finished, this, [=]() { + info_msg(QStringLiteral("Uncompression finished : %1.").arg(errorString())); + watcher->deleteLater(); + emit finished(); + }); + + watcher->setFuture(QtConcurrent::run(this, &GZipUncompressor::doUncompress)); +} + +GZipUncompressor::~GZipUncompressor() +{} + +double GZipUncompressor::progress() const +{ + return m_progress; +} + +void GZipUncompressor::setProgress(double progress) +{ + if(qFuzzyCompare(m_progress, progress)) { + return; + } + + m_progress = progress; + emit progressChanged(); +} + +void GZipUncompressor::doUncompress() +{ + if(m_in->bytesAvailable() <= 4) { + setError(QStringLiteral("The input file is empty")); + return; + } + + const auto totalSize = m_in->bytesAvailable(); + info_msg(QStringLiteral("Uncompressing file with size of %1 bytes...").arg(totalSize)); + + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = 0; + stream.next_in = Z_NULL; + + const auto err = inflateInit2(&stream, 15 + 16); + if(err != Z_OK) { + setError(QStringLiteral("Failed to initialise deflate method")); + return; + } + + char inbuf[CHUNK_SIZE]; + char outbuf[CHUNK_SIZE]; + + do { + const auto n = m_in->read(inbuf, CHUNK_SIZE); + stream.avail_in = n; + stream.next_in = (Bytef*)inbuf; + + do { + stream.avail_out = CHUNK_SIZE; + stream.next_out = (Bytef*)outbuf; + + const auto err = inflate(&stream, Z_NO_FLUSH); + const auto errorOccured = (err == Z_MEM_ERROR) || (err == Z_DATA_ERROR) || (err == Z_NEED_DICT); + + if(errorOccured) { + inflateEnd(&stream); + setError(QStringLiteral("Error during uncompression")); + return; + } + + m_out->write(outbuf, CHUNK_SIZE - stream.avail_out); + + } while(!stream.avail_out); + + setProgress(progress() + (100.0 * n) / totalSize); + + } while(m_in->bytesAvailable()); + + inflateEnd(&stream); + + if(!m_in->seek(0) || !m_out->seek(0)) { + setError(QStringLiteral("Failed to rewind input files")); + } +} diff --git a/backend/gzipuncompressor.h b/backend/gzipuncompressor.h new file mode 100644 index 00000000..b6cd1afd --- /dev/null +++ b/backend/gzipuncompressor.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "failable.h" + +class QIODevice; + +class GZipUncompressor : public QObject, public Failable +{ + Q_OBJECT + +public: + GZipUncompressor(QIODevice *in, QIODevice* out, QObject *parent = nullptr); + ~GZipUncompressor(); + + double progress() const; + +signals: + void finished(); + void progressChanged(); + +private: + void setProgress(double progress); + void doUncompress(); + + QIODevice *m_in; + QIODevice *m_out; + + double m_progress; +}; + + diff --git a/backend/qflipperbackend.cpp b/backend/qflipperbackend.cpp index c9ea2406..37835630 100644 --- a/backend/qflipperbackend.cpp +++ b/backend/qflipperbackend.cpp @@ -1,6 +1,7 @@ #include "qflipperbackend.h" #include "flipperzero/flipperzero.h" +#include "flipperzero/assetmanifest.h" #include "flipperzero/remotecontroller.h" #include "flipperzero/recoverycontroller.h" @@ -17,6 +18,9 @@ QFlipperBackend::QFlipperBackend(): qRegisterMetaType("Flipper::Updates::ChannelInfo"); qRegisterMetaType("Flipper::Zero::RemoteController*"); qRegisterMetaType("Flipper::Zero::RecoveryController*"); + + qRegisterMetaType(); + QMetaType::registerComparators(); } QFlipperBackend::~QFlipperBackend() diff --git a/backend/signalingfailable.h b/backend/signalingfailable.h new file mode 100644 index 00000000..bc61640a --- /dev/null +++ b/backend/signalingfailable.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "failable.h" + +class SignalingFailable : public QObject, public Failable +{ + Q_OBJECT + +public: + SignalingFailable(QObject *parent = nullptr): + QObject(parent) {}; + + void setError(const QString &errorMessage) override { + Failable::setError(errorMessage); + emit errorOccured(); + } + +signals: + void errorOccured(); +}; diff --git a/backend/simpleserialoperation.cpp b/backend/simpleserialoperation.cpp new file mode 100644 index 00000000..019b80e6 --- /dev/null +++ b/backend/simpleserialoperation.cpp @@ -0,0 +1,68 @@ +#include "simpleserialoperation.h" + +#include + +SimpleSerialOperation::SimpleSerialOperation(QSerialPort *serialPort, QObject *parent): + AbstractSerialOperation(serialPort, parent) +{} + +const QByteArray &SimpleSerialOperation::receivedData() const +{ + return m_receivedData; +} + +QByteArray SimpleSerialOperation::commandLine() const +{ + // Empty default implementation + return QByteArray(); +} + +uint32_t SimpleSerialOperation::flags() const +{ + // Empty default implementation + return 0; +} + +void SimpleSerialOperation::onSerialPortReadyRead() +{ + startTimeout(); + + m_receivedData += serialPort()->readAll(); + + if(m_receivedData.endsWith(endOfMessageToken())) { + if(!parseReceivedData()) { + finishWithError(QStringLiteral("Failed to parse received data")); + } else { + finish(); + } + } +} + +bool SimpleSerialOperation::begin() +{ + auto success = true; + + if(flags() & DTR) { + success &= serialPort()->setDataTerminalReady(true); + } + + if(flags() & RTS) { + success &= serialPort()->setRequestToSend(flags() & RTS); + } + + if(!commandLine().isEmpty()) { + success &= serialPort()->write(commandLine()) == commandLine().size(); + } + + if(success) { + startTimeout(); + } + + return success; +} + +bool SimpleSerialOperation::parseReceivedData() +{ + // Empty default implementation + return true; +} diff --git a/backend/simpleserialoperation.h b/backend/simpleserialoperation.h new file mode 100644 index 00000000..4bbc77a8 --- /dev/null +++ b/backend/simpleserialoperation.h @@ -0,0 +1,35 @@ +#pragma once + +#include "abstractserialoperation.h" + +#include + +class SimpleSerialOperation : public AbstractSerialOperation +{ + Q_OBJECT + +public: + enum Flags { + DTR = (1 << 0), + RTS = (1 << 2) + }; + + SimpleSerialOperation(QSerialPort *serialPort, QObject *parent = nullptr); + +protected: + const QByteArray &receivedData() const; + + virtual QByteArray endOfMessageToken() const = 0; + virtual QByteArray commandLine() const; + virtual uint32_t flags() const; + + virtual bool parseReceivedData(); + +private slots: + void onSerialPortReadyRead() override; + +private: + bool begin() override; + + QByteArray m_receivedData; +}; diff --git a/backend/tararchive.cpp b/backend/tararchive.cpp new file mode 100644 index 00000000..b7da8fd4 --- /dev/null +++ b/backend/tararchive.cpp @@ -0,0 +1,128 @@ +#include "tararchive.h" + +#include + +#include "macros.h" + +#define BLOCK_SIZE 512 + +struct TarHeader +{ + char name[100]; + char unused1[24]; + char size[12]; + char unused2[20]; + char typeflag; + char unused3[100]; + char magic[6]; + char unused4[249]; +}; + +static_assert(sizeof(TarHeader) == BLOCK_SIZE, "Check TarHeader alignment"); + +static bool isMemZeros(char *p, size_t len) +{ + while(len--) { + if(*(p++)) { + return false; + } + } + + return true; +} + +TarArchive::TarArchive(): + m_tarFile(nullptr) +{} + +TarArchive::TarArchive(QIODevice *file): + m_tarFile(file), + m_root(new FileNode("", FileNode::Type::Directory)) +{ + buildIndex(); +} + +FileNode *TarArchive::file(const QString &fullName) +{ + return m_root->find(fullName); +} + +QByteArray TarArchive::fileData(const QString &fullName) +{ + if(!m_tarFile) { + setError(QStringLiteral("Archive file not set")); + return QByteArray(); + } + + auto *node = file(fullName); + if(!node) { + setError(QStringLiteral("File not found")); + return QByteArray(); + } + + if(!node->userData().canConvert()) { + setError(QStringLiteral("No valid FileData found in the node.")); + return QByteArray(); + } + + const auto data = node->userData().value(); + const auto success = m_tarFile->seek(data.offset); + + if(success) { + return m_tarFile->read(data.size); + + } else { + setError(m_tarFile->errorString()); + return QByteArray(); + } +} + +void TarArchive::buildIndex() +{ + TarHeader header; + int emptyCounter = 0; + + do { + const auto n = m_tarFile->read((char*)&header, sizeof(TarHeader)); + + if(n != sizeof(TarHeader)) { + setError(QStringLiteral("Archive file is truncated")); + return; + + } else if(isMemZeros((char*)&header, sizeof(TarHeader))) { + if(++emptyCounter == 2) { + break; + } else { + continue; + } + + } else if(strncmp(header.magic, "ustar", 5)) { + setError(QStringLiteral("Tar magic constant not found.")); + return; + } + + const auto fileSize = strtol(header.size, nullptr, 8); + const auto fileName = QString(header.name); + + if(header.typeflag == '0') { + FileInfo data; + + data.offset = m_tarFile->pos(); + data.size = fileSize; + + m_root->addFile(fileName, QVariant::fromValue(data)); + + } else if(header.typeflag == '5') { + m_root->addDirectory(fileName.chopped(1)); + + } else { + setError(QStringLiteral("Only regular files and directories are supported")); + return; + } + + // Blocks are always padded to BLOCK_SIZE + const auto padding = fileSize % BLOCK_SIZE ? BLOCK_SIZE - (fileSize % BLOCK_SIZE) : 0; + m_tarFile->skip(fileSize + padding); + + } while(m_tarFile->bytesAvailable()); +} diff --git a/backend/tararchive.h b/backend/tararchive.h new file mode 100644 index 00000000..ad72b8e5 --- /dev/null +++ b/backend/tararchive.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include "filenode.h" +#include "failable.h" + +class QIODevice; + +class TarArchive : public Failable +{ +public: + struct FileInfo { + qint64 offset; + qint64 size; + }; + + TarArchive(); + TarArchive(QIODevice *file); + + FileNode *file(const QString &fullName); + QByteArray fileData(const QString &fullName); + +private: + void buildIndex(); + + QIODevice *m_tarFile; + QSharedPointer m_root; +}; + +Q_DECLARE_METATYPE(TarArchive::FileInfo); diff --git a/qflipper_common.pri b/qflipper_common.pri index 31ed5a5b..a1065558 100644 --- a/qflipper_common.pri +++ b/qflipper_common.pri @@ -3,12 +3,13 @@ NAME = qFlipper unix:!macx { DEFINES += USB_BACKEND_LIBUSB CONFIG += link_pkgconfig - PKGCONFIG += libusb-1.0 + PKGCONFIG += libusb-1.0 zlib } else:win32 { CONFIG -= debug_and_release DEFINES += USB_BACKEND_WIN32 - + INCLUDEPATH += $$[QT_INSTALL_HEADERS]/QtZlib + !win32-g++: LIBS += -lSetupApi -lWinusb -lUser32 else: LIBS += -lsetupapi -lwinusb @@ -16,7 +17,7 @@ unix:!macx { DEFINES += USB_BACKEND_LIBUSB PKG_CONFIG = /usr/local/bin/pkg-config CONFIG += link_pkgconfig - PKGCONFIG += libusb-1.0 + PKGCONFIG += libusb-1.0 zlib } else { error("Unsupported OS or compiler")