Repository navigation
feat: add folder content provider registry #1781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| #include "foldercontent.h" | ||
|
|
||
| #include <QCoreApplication> | ||
| #include <QDebug> | ||
| #include <QDir> | ||
| #include <QFileInfo> | ||
| #include <QPluginLoader> | ||
| #include <QRegularExpression> | ||
|
|
||
| DS_BEGIN_NAMESPACE | ||
|
|
||
| FolderContentRegistry *FolderContentRegistry::instance() | ||
| { | ||
| static auto registry = new FolderContentRegistry(qApp); | ||
| return registry; | ||
| } | ||
|
|
||
| QString FolderContentRegistry::reference(const QString &source, const QString &id) | ||
| { | ||
| return QStringLiteral("folder/%1/%2").arg(source, QString::fromLatin1(id.toUtf8().toBase64( | ||
| QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))); | ||
| } | ||
|
|
||
| bool FolderContentRegistry::decode(const QString &value, QString *source, QString *id) | ||
| { | ||
| const auto parts = value.split('/'); | ||
| static const QRegularExpression sourcePattern(QStringLiteral("^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")); | ||
| if (parts.size() != 3 || parts[0] != "folder" || !sourcePattern.match(parts[1]).hasMatch()) | ||
| return false; | ||
| const auto bytes = QByteArray::fromBase64(parts[2].toLatin1(), | ||
| QByteArray::Base64UrlEncoding | QByteArray::AbortOnBase64DecodingErrors); | ||
| *source = parts[1]; | ||
| *id = QString::fromUtf8(bytes); | ||
| return !id->isEmpty() && reference(*source, *id) == value; | ||
| } | ||
|
|
||
| bool FolderContentRegistry::isReference(const QString &value) | ||
| { | ||
| QString source, id; | ||
| return decode(value, &source, &id); | ||
| } | ||
|
|
||
| QString FolderContentRegistry::referenceWithId(const QString &value, const QString &id) const | ||
| { | ||
| QString source, current; | ||
| if (id.isEmpty() || !decode(value, &source, ¤t)) return {}; | ||
| return reference(source, id); | ||
| } | ||
|
|
||
| FolderContentProvider *FolderContentRegistry::provider(const QString &source) | ||
| { | ||
| if (m_providers.contains(source)) | ||
| return m_providers.value(source); | ||
|
|
||
| // Content providers live in the regular shell plugin directory and follow | ||
| // the same file naming as every other shell plugin: | ||
| // org.deepin.ds.folderprovider.<source>.so | ||
| const auto fileName = QStringLiteral("org.deepin.ds.folderprovider.%1.so").arg(source); | ||
| QStringList dirs { | ||
| QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("../plugins"), | ||
| QStringLiteral(DDE_SHELL_PLUGIN_INSTALL_DIR) | ||
| }; | ||
| const auto runtimePaths = qEnvironmentVariable("DDE_SHELL_PLUGIN_PATH"); | ||
| if (!runtimePaths.isEmpty()) | ||
| dirs.append(runtimePaths.split(QDir::listSeparator(), Qt::SkipEmptyParts)); | ||
| dirs.append(QCoreApplication::libraryPaths()); | ||
|
|
||
| for (const auto &dir : dirs) { | ||
| const auto path = QDir(dir).filePath(fileName); | ||
| if (!QFileInfo::exists(path)) | ||
| continue; | ||
| auto loader = new QPluginLoader(path, this); | ||
| auto object = loader->instance(); | ||
| auto contentProvider = qobject_cast<FolderContentProvider *>(object); | ||
| if (contentProvider) { | ||
| m_providers.insert(source, contentProvider); | ||
| return contentProvider; | ||
|
Comment on lines
+54
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Concurrent lookups race on providers When two threads call Serialize registry operations on the registry's owning thread, or protect provider lookup/loading and cache updates with an appropriate synchronization strategy. Prompt for AI agentsIn `frame/foldercontent.cpp` at lines 54-79:
**Concurrent lookups race on providers**
When two threads call `create()` or `normalizeReference()` on the singleton at the same time, `provider()` checks and updates the shared `m_providers` hash without synchronization, so concurrent `create()` or `normalizeReference()` calls can race while loading and inserting providers, corrupting the cache or returning inconsistent results. Constructing a `QPluginLoader` with the registry as parent from a non-owner thread also violates QObject thread-affinity rules.
Serialize registry operations on the registry's owning thread, or protect provider lookup/loading and cache updates with an appropriate synchronization strategy. |
||
| } | ||
| qWarning() << "Failed to load folder content provider:" << path | ||
| << (object ? QStringLiteral("FolderContentProvider interface mismatch") : loader->errorString()); | ||
| delete loader; | ||
| } | ||
| qWarning() << "Folder content provider unavailable:" << fileName << "search paths:" << dirs; | ||
| return nullptr; | ||
| } | ||
|
|
||
| QString FolderContentRegistry::normalizeReference(const QString &value) | ||
| { | ||
| QString source, id; | ||
| if (!decode(value, &source, &id)) return {}; | ||
| auto factory = provider(source); | ||
| const auto normalized = factory ? factory->normalizeId(id) : QString(); | ||
| return normalized.isEmpty() ? QString() : reference(source, normalized); | ||
| } | ||
|
|
||
| FolderPresentation *FolderContentRegistry::create(const QString &value, QObject *parent) | ||
| { | ||
| QString source, id; | ||
| if (!decode(value, &source, &id)) return nullptr; | ||
| if (auto factory = provider(source)) | ||
| return factory->create(id, parent); | ||
| // Keep pins when a provider is temporarily unavailable. | ||
| return new FolderPresentation(parent); | ||
| } | ||
|
|
||
| DS_END_NAMESPACE | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| #pragma once | ||
|
|
||
| #include "dsglobal.h" | ||
| #include <QObject> | ||
| #include <QUrl> | ||
| #include <QVariantList> | ||
| #include <QHash> | ||
|
|
||
| DS_BEGIN_NAMESPACE | ||
|
|
||
| // Metadata for the pinned icon only. The provider's QML view owns all content | ||
| // and operations; the framework and task manager do not model its entries. | ||
| class DS_SHARE FolderPresentation : public QObject | ||
| { | ||
| Q_OBJECT | ||
| Q_PROPERTY(QString title READ title NOTIFY presentationChanged) | ||
| Q_PROPERTY(QVariantList previewIcons READ previewIcons NOTIFY presentationChanged) | ||
| Q_PROPERTY(QUrl iconUrl READ iconUrl CONSTANT) | ||
| Q_PROPERTY(QUrl contentUrl READ contentUrl CONSTANT) | ||
| public: | ||
| using QObject::QObject; | ||
| virtual QString title() const { return tr("Folder"); } | ||
| virtual QVariantList previewIcons() const { return {}; } | ||
| virtual QUrl iconUrl() const { return {}; } | ||
| virtual QUrl contentUrl() const { return {}; } | ||
| signals: | ||
| void presentationChanged(); | ||
| void identityChanged(const QString &id); | ||
| void contentLost(); | ||
| }; | ||
|
|
||
| class FolderContentProvider | ||
| { | ||
| public: | ||
| virtual ~FolderContentProvider() = default; | ||
| virtual QString normalizeId(const QString &id) const = 0; | ||
| virtual FolderPresentation *create(const QString &id, QObject *parent) = 0; | ||
| }; | ||
|
|
||
| class DS_SHARE FolderContentRegistry : public QObject | ||
| { | ||
| Q_OBJECT | ||
| public: | ||
| using QObject::QObject; | ||
| static FolderContentRegistry *instance(); | ||
| static bool isReference(const QString &reference); | ||
| static QString reference(const QString &source, const QString &id); | ||
| // Rebuild an existing reference with a new provider id, keeping its source. | ||
| QString referenceWithId(const QString &reference, const QString &id) const; | ||
| QString normalizeReference(const QString &reference); | ||
| FolderPresentation *create(const QString &reference, QObject *parent); | ||
| private: | ||
| FolderContentProvider *provider(const QString &source); | ||
| static bool decode(const QString &reference, QString *source, QString *id); | ||
| QHash<QString, FolderContentProvider *> m_providers; | ||
| }; | ||
|
|
||
| DS_END_NAMESPACE | ||
|
|
||
| #define DS_FOLDER_CONTENT_PROVIDER_IID "org.deepin.ds.FolderContentProvider/1.0" | ||
| Q_DECLARE_INTERFACE(DS_NAMESPACE::FolderContentProvider, DS_FOLDER_CONTENT_PROVIDER_IID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Generated folder reference is unusable
When a caller passes an empty ID or a source that fails the accepted pattern,
reference()encodes the arguments without checking the decoder's constraints, sodecode()rejects the returned value and callers cannot use it withisReference()orcreate().Make
reference()reject empty IDs and sources that fail the decoder's pattern before constructing a reference.Prompt for AI agents