Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frame/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ set(PUBLIC_HEADERS
wayland/xdgactivation.h
models/listtotableproxymodel.h
dsutility.h
foldercontent.h
)

set(PRIVATE_HEADERS
Expand Down Expand Up @@ -77,6 +78,7 @@ add_library(dde-shell-frame SHARED
popupwindow.cpp
ddeshell_qml.qrc
dsutility.cpp
foldercontent.cpp
)

set_target_properties(dde-shell-frame PROPERTIES
Expand Down
108 changes: 108 additions & 0 deletions frame/foldercontent.cpp
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>

Check warning on line 5 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QCoreApplication> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QDebug>

Check warning on line 6 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDebug> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QDir>

Check warning on line 7 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QFileInfo>

Check warning on line 8 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QFileInfo> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QPluginLoader>

Check warning on line 9 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QPluginLoader> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QRegularExpression>

Check warning on line 10 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QRegularExpression> not found. Please note: Cppcheck does not need standard library headers to get proper results.

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(

Copy link
Copy Markdown

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, so decode() rejects the returned value and callers cannot use it with isReference() or create().

Make reference() reject empty IDs and sources that fail the decoder's pattern before constructing a reference.

Prompt for AI agents
In `frame/foldercontent.cpp` at line 22:

**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, so `decode()` rejects the returned value and callers cannot use it with `isReference()` or `create()`.

Make `reference()` reject empty IDs and sources that fail the decoder's pattern before constructing a reference.

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)

Check warning on line 39 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'isReference' is never used.
{
QString source, id;
return decode(value, &source, &id);
}

QString FolderContentRegistry::referenceWithId(const QString &value, const QString &id) const

Check warning on line 45 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'referenceWithId' is never used.
{
QString source, current;
if (id.isEmpty() || !decode(value, &source, &current)) 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · 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.

Prompt for AI agents
In `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)

Check warning on line 89 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'normalizeReference' is never used.
{
QString source, id;
if (!decode(value, &source, &id)) return {};
auto factory = provider(source);

Check warning on line 93 in frame/foldercontent.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Variable 'factory' can be declared as pointer to const
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
63 changes: 63 additions & 0 deletions frame/foldercontent.h
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)
Loading