Skip to content

Commit

Permalink
Avoid using QPointer with QMimeData
Browse files Browse the repository at this point in the history
This does not seem safe.
  • Loading branch information
hluk committed Nov 18, 2024
1 parent f8f0b06 commit 9463694
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 205 deletions.
184 changes: 184 additions & 0 deletions src/common/clipboarddataguard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "common/clipboarddataguard.h"
#include "common/common.h"
#include "common/log.h"
#include "common/mimetypes.h"
#include "common/textdata.h"

#include <QImage>
#include <QList>
#include <QMimeData>
#include <QObject>
#include <QUrl>
#include <QCoreApplication>

namespace {

int clipboardCopyTimeoutMs()
{
static bool ok = false;
static int ms = qgetenv("COPYQ_CLIPBOARD_COPY_TIMEOUT_MS").toInt(&ok);
return ok ? ms : 5000;
}

const QMimeData *dummyMimeData()
{
static QMimeData mimeData;
return &mimeData;
}

class ElapsedGuard {
public:
explicit ElapsedGuard(const QString &type, const QString &format)
: m_type(type)
, m_format(format)
{
COPYQ_LOG_VERBOSE( QStringLiteral("Accessing [%1:%2]").arg(type, format) );
m_elapsed.start();
}

~ElapsedGuard()
{
const auto t = m_elapsed.elapsed();
if (t > 500)
log( QStringLiteral("ELAPSED %1 ms accessing [%2:%3]").arg(t).arg(m_type, m_format), LogWarning );
}
private:
QString m_type;
QString m_format;
QElapsedTimer m_elapsed;
};

} //namespace

ClipboardDataGuard::ClipboardDataGuard(const QMimeData *data, const long int *clipboardSequenceNumber)
: m_data(data)
, m_clipboardSequenceNumber(clipboardSequenceNumber)
, m_clipboardSequenceNumberOriginal(clipboardSequenceNumber ? *clipboardSequenceNumber : 0)
{
// This uses simple connection to ensure pointer is not destroyed
// instead of QPointer to work around a possible Qt bug:
// - https://bugzilla.redhat.com/show_bug.cgi?id=2320093
// - https://bugzilla.redhat.com/show_bug.cgi?id=2326881
m_connection = QObject::connect(m_data, &QObject::destroyed, [this](){
m_data = nullptr;
log( QByteArrayLiteral("Aborting clipboard cloning: Data deleted"), LogWarning );
});
m_timerExpire.start();
}

ClipboardDataGuard::~ClipboardDataGuard()
{
QObject::disconnect(m_connection);
}

QStringList ClipboardDataGuard::formats()
{
ElapsedGuard _(QStringLiteral(), QStringLiteral("formats"));
return mimeData()->formats();
}

bool ClipboardDataGuard::hasFormat(const QString &mime)
{
ElapsedGuard _(QStringLiteral("hasFormat"), mime);
return mimeData()->hasFormat(mime);
}

QByteArray ClipboardDataGuard::data(const QString &mime)
{
ElapsedGuard _(QStringLiteral("data"), mime);
return mimeData()->data(mime);
}

QList<QUrl> ClipboardDataGuard::urls()
{
ElapsedGuard _(QStringLiteral(), QStringLiteral("urls"));
return mimeData()->urls();
}

QString ClipboardDataGuard::text()
{
ElapsedGuard _(QStringLiteral(), QStringLiteral("text"));
return mimeData()->text();
}

bool ClipboardDataGuard::hasText()
{
ElapsedGuard _(QStringLiteral(), QStringLiteral("hasText"));
return mimeData()->hasText();
}

QImage ClipboardDataGuard::getImageData()
{
ElapsedGuard _(QStringLiteral(), QStringLiteral("imageData"));

// NOTE: Application hangs if using multiple sessions and
// calling QMimeData::hasImage() on X11 clipboard.
QImage image = mimeData()->imageData().value<QImage>();
if ( image.isNull() ) {
image.loadFromData( data(QStringLiteral("image/png")), "png" );
if ( image.isNull() ) {
image.loadFromData( data(QStringLiteral("image/bmp")), "bmp" );
}
}
COPYQ_LOG_VERBOSE(
QStringLiteral("Image is %1")
.arg(image.isNull() ? QStringLiteral("invalid") : QStringLiteral("valid")) );
return image;
}

QByteArray ClipboardDataGuard::getUtf8Data(const QString &format)
{
ElapsedGuard _(QStringLiteral("UTF8"), format);

if (format == mimeUriList) {
QByteArray bytes;
for ( const auto &url : urls() ) {
if ( !bytes.isEmpty() )
bytes += '\n';
bytes += url.toString().toUtf8();
}
return bytes;
}

if ( format == mimeText && !hasFormat(mimeText) )
return text().toUtf8();

if ( format.startsWith(QLatin1String("text/")) )
return dataToText( data(format), format ).toUtf8();

return data(format);
}

bool ClipboardDataGuard::isExpired() {
if (!m_data)
return true;

if (m_clipboardSequenceNumber && *m_clipboardSequenceNumber != m_clipboardSequenceNumberOriginal) {
m_data = nullptr;
log( QByteArrayLiteral("Aborting clipboard cloning: Clipboard changed again"), LogWarning );
return true;
}

if (m_timerExpire.elapsed() > clipboardCopyTimeoutMs()) {
m_data = nullptr;
log( QByteArrayLiteral("Aborting clipboard cloning: Data access took too long"), LogWarning );
return true;
}

return false;
}

const QMimeData *ClipboardDataGuard::mimeData()
{
if (isExpired())
return dummyMimeData();

if (m_timerExpire.elapsed() > 100) {
QCoreApplication::processEvents();
if (isExpired())
return dummyMimeData();
}

return m_data;
}
40 changes: 40 additions & 0 deletions src/common/clipboarddataguard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include <QElapsedTimer>
#include <QtContainerFwd>
#include <QMetaType>

class QMimeData;
class QImage;
class QUrl;

// Avoids accessing old clipboard/drag'n'drop data.
class ClipboardDataGuard final {
public:
explicit ClipboardDataGuard(
const QMimeData *data,
const long int *clipboardSequenceNumber = nullptr);

~ClipboardDataGuard();

QStringList formats();
bool hasFormat(const QString &mime);

QByteArray data(const QString &mime);

QList<QUrl> urls();
QString text();
bool hasText();
QImage getImageData();
QByteArray getUtf8Data(const QString &format);
bool isExpired();
const QMimeData *mimeData();

private:
const QMimeData *m_data;
QElapsedTimer m_timerExpire;
const long int *m_clipboardSequenceNumber;
long int m_clipboardSequenceNumberOriginal;
QMetaObject::Connection m_connection;
};
Loading

0 comments on commit 9463694

Please sign in to comment.