on xenial help sign in
Rewrite WindowStateStorage for improved async power

6931e29 changed WindowStateStorage from asynchronous writes to synchronous due to changes in QSql that denied reusing connections in different threads. This commit splits out all QSql functionality from the WindowStateStorage object into a separate AsyncQuery object and moves the AsyncQuery object into another thread. This allows performing write operations in another thread, preventing main thread freezes from slow IO. If database initialization fails, the object simply returns the default values provided and black-holes writes. Also adds tests to ensure the database name is being used sanely and that SQL injection is guarded against. Co-Authored-By: Rodney Dawes <dobey@ubports.com>

commitadd6528f9c3963ce071355aed7cf3cd2c9a147cd authorDalton Durst <dalton@ubports.com> dateSat 16 Jan 2021, 22:48 (6 years ago) parentf821fd2
3 files changed+347−108
plugins/Utils/windowstatestorage.cpp+253−98 plugins/Utils/windowstatestorage.h+47−10 tests/plugins/Utils/WindowStateStorageTest.cpp+47
plugins/Utils/windowstatestorage.cpp+253−98
@@ -1,5 +1,6 @@
11/*
22 * Copyright 2015-2016 Canonical Ltd.
3+ * Copyright 2021 UBports Foundation
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU Lesser General Public License as published by
@@ -16,167 +17,319 @@
1617
1718#include "windowstatestorage.h"
1819
19−#include <QtConcurrent>
2020#include <QDebug>
21+#include <QDir>
22+#include <QMetaObject>
23+#include <QObject>
2124#include <QSqlQuery>
2225#include <QSqlError>
2326#include <QSqlResult>
27+#include <QStandardPaths>
2428#include <QRect>
2529#include <unity/shell/application/ApplicationInfoInterface.h>
2630
27−QMutex WindowStateStorage::s_mutex;
31+AsyncQuery::AsyncQuery(const QString& dbName):
32+ m_dbName(dbName)
33+{
34+}
2835
29−inline QString sanitiseString(QString string) {
30− return string.remove(QLatin1Char('\"'))
31− .remove(QLatin1Char('\''))
32− .remove(QLatin1Char('\\'));
36+AsyncQuery::~AsyncQuery()
37+{
38+ QSqlDatabase::removeDatabase(m_connectionName);
3339}
3440
35−WindowStateStorage::WindowStateStorage(const QString& dbName, QObject *parent):
36− QObject(parent)
37−{
38− const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
39− m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"));
40− QDir dir;
41− dir.mkpath(dbPath);
42− if (dbName != nullptr) {
43− m_db.setDatabaseName(dbName);
44− } else {
45− m_db.setDatabaseName(dbPath + "windowstatestorage.sqlite");
41+bool AsyncQuery::initdb()
42+{
43+ QSqlDatabase connection = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
44+ connection.setDatabaseName(m_dbName);
45+ if (!connection.open()) {
46+ qWarning() << "Error opening state database " << m_dbName << connection.lastError().driverText() << connection.lastError().databaseText();
47+ return false;
48+ }
49+ QSqlQuery query(connection);
50+
51+ if (!connection.tables().contains(QStringLiteral("geometry"))) {
52+ QString geometryQuery = QStringLiteral("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);");
53+ if (!query.exec(geometryQuery)) {
54+ logSqlError(query);
55+ return false;
56+ }
4657 }
47− initdb();
58+
59+ if (!connection.tables().contains(QStringLiteral("state"))) {
60+ QString stateQuery = QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);");
61+ if (!query.exec(stateQuery)) {
62+ logSqlError(query);
63+ return false;
64+ }
65+ }
66+
67+ if (!connection.tables().contains(QStringLiteral("stage"))) {
68+ QString stageQuery = QStringLiteral("CREATE TABLE stage(appId TEXT UNIQUE, stage INTEGER);");
69+ if (!query.exec(stageQuery)) {
70+ logSqlError(query);
71+ return false;
72+ }
73+ }
74+ return true;
4875}
4976
50−WindowStateStorage::~WindowStateStorage()
77+void AsyncQuery::saveState(const QString &windowId, int state)
5178{
52− m_db.close();
79+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
80+ QSqlQuery query(connection);
81+ query.prepare(m_saveStateQuery);
82+ query.bindValue(":windowId", windowId);
83+ query.bindValue(":state", state);
84+ if (!query.exec()) {
85+ logSqlError(query);
86+ }
5387}
5488
55−void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
89+int AsyncQuery::getState(const QString &windowId) const
5690{
57− const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
58− .arg(sanitiseString(windowId))
59− .arg((int)state);
60−
61− saveValue(queryString);
91+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
92+ QSqlQuery query(connection);
93+ query.prepare(m_getStateQuery);
94+ query.bindValue(":windowId", windowId);
95+ query.exec();
96+ if (!query.isActive() || !query.isSelect()) {
97+ logSqlError(query);
98+ return -1;
99+ }
100+ if (!query.first()) {
101+ return -1;
102+ }
103+ bool converted = false;
104+ QVariant resultStr = query.value(0);
105+ int result = resultStr.toInt(&converted);
106+ if (converted) {
107+ return result;
108+ } else {
109+ qWarning() << "getState result expected integer, got " << resultStr;
110+ return -1;
111+ }
62112}
63113
64−WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
114+void AsyncQuery::saveGeometry(const QString &windowId, const QRect &rect)
65115{
66− const QString queryString = QStringLiteral("SELECT state FROM state WHERE windowId = '%1';")
67− .arg(sanitiseString(windowId));
116+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
117+ QSqlQuery query(connection);
118+ query.prepare(m_saveGeometryQuery);
119+ query.bindValue(":windowId", windowId);
120+ query.bindValue(":x", rect.x());
121+ query.bindValue(":y", rect.y());
122+ query.bindValue(":width", rect.width());
123+ query.bindValue(":height", rect.height());
124+ if (!query.exec()) {
125+ logSqlError(query);
126+ }
127+}
68128
69− QSqlQuery query = getValue(queryString);
129+QRect AsyncQuery::getGeometry(const QString &windowId) const
130+{
131+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
132+ QSqlQuery query(connection);
133+ query.prepare(m_getGeometryQuery);
134+ query.bindValue(":windowId", windowId);
135+ query.exec();
136+ if (!query.isActive() || !query.isSelect()) {
137+ logSqlError(query);
138+ return QRect();
139+ }
70140
71141 if (!query.first()) {
72− return defaultValue;
142+ return QRect();
73143 }
74− return (WindowState)query.value(QStringLiteral("state")).toInt();
75−}
76144
77−void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
78−{
79− const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
80− .arg(sanitiseString(windowId))
81− .arg(rect.x())
82− .arg(rect.y())
83− .arg(rect.width())
84− .arg(rect.height());
145+ bool xConverted, yConverted, widthConverted, heightConverted = false;
146+ int x, y, width, height;
147+ QVariant xResultStr = query.value(QStringLiteral("x"));
148+ QVariant yResultStr = query.value(QStringLiteral("y"));
149+ QVariant widthResultStr = query.value(QStringLiteral("width"));
150+ QVariant heightResultStr = query.value(QStringLiteral("height"));
151+ x = xResultStr.toInt(&xConverted);
152+ y = yResultStr.toInt(&yConverted);
153+ width = widthResultStr.toInt(&widthConverted);
154+ height = heightResultStr.toInt(&heightConverted);
155+
156+ if (xConverted && yConverted && widthConverted && heightConverted) {
157+ return QRect(x, y, width, height);
158+ } else {
159+ qWarning() << "getGeometry result expected integers, got x:"
160+ << xResultStr << "y:" << yResultStr << "width" << widthResultStr
161+ << "height:" << heightResultStr;
162+ return QRect();
163+ }
85164
86− saveValue(queryString);
87165}
88166
89−void WindowStateStorage::saveStage(const QString &appId, int stage)
167+void AsyncQuery::saveStage(const QString &appId, int stage)
90168{
91− const QString queryString = QStringLiteral("INSERT OR REPLACE INTO stage (appId, stage) values ('%1', '%2');")
92− .arg(sanitiseString(appId))
93− .arg(stage);
169+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
170+ QSqlQuery query(connection);
171+ query.prepare(m_saveStageQuery);
172+ query.bindValue(":appId", appId);
173+ query.bindValue(":stage", stage);
174+ if (!query.exec()) {
175+ logSqlError(query);
176+ }
177+}
94178
95− saveValue(queryString);
179+int AsyncQuery::getStage(const QString &appId) const
180+{
181+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
182+ QSqlQuery query(connection);
183+ query.prepare(m_getStageQuery);
184+ query.bindValue(":appId", appId);
185+ query.exec();
186+ if (!query.isActive() || !query.isSelect()) {
187+ logSqlError(query);
188+ return -1;
189+ }
190+ if (!query.first()) {
191+ return -1;
192+ }
193+ bool converted = false;
194+ QVariant resultStr = query.value(0);
195+ int result = resultStr.toInt(&converted);
196+ if (converted) {
197+ return result;
198+ } else {
199+ qWarning() << "getStage result expected integer, got " << resultStr;
200+ return -1;
201+ }
96202}
97203
98−int WindowStateStorage::getStage(const QString &appId, int defaultValue) const
204+void AsyncQuery::logSqlError(const QSqlQuery query) const
99205{
100− const QString queryString = QStringLiteral("SELECT stage FROM stage WHERE appId = '%1';")
101− .arg(sanitiseString(appId));
206+ qWarning() << "Error executing query" << query.lastQuery()
207+ << "Driver error:" << query.lastError().driverText()
208+ << "Database error:" << query.lastError().databaseText();
209+}
102210
103− QSqlQuery query = getValue(queryString);
211+const QString AsyncQuery::getDbName()
212+{
213+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
214+ return connection.databaseName();
215+}
104216
105− if (!query.first()) {
106− return defaultValue;
217+WindowStateStorage::WindowStateStorage(const QString& dbName, QObject *parent):
218+ QObject(parent),
219+ m_thread()
220+{
221+ QString dbFile;
222+ if (dbName == nullptr) {
223+ const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
224+ QDir dir;
225+ dir.mkpath(dbPath);
226+ dbFile = QString(dbPath) + QStringLiteral("windowstatestorage.sqlite");
227+ } else {
228+ dbFile = dbName;
229+ }
230+ m_asyncQuery = new AsyncQuery(dbFile);
231+ m_asyncQuery->moveToThread(&m_thread);
232+ connect(&m_thread, &QThread::finished, m_asyncQuery, &QObject::deleteLater);
233+ m_thread.start();
234+ bool queryInitSuccessful;
235+ QMetaObject::invokeMethod(m_asyncQuery, "initdb",
236+ Qt::BlockingQueuedConnection,
237+ Q_RETURN_ARG(bool, queryInitSuccessful));
238+ if (!queryInitSuccessful) {
239+ qWarning() << "Failed to initialize WindowStateStorage! Windows will not be restored to their previous location.";
240+ m_asyncOk = false;
241+ m_thread.quit();
242+ } else {
243+ connect(this, &WindowStateStorage::saveGeometry, m_asyncQuery, &AsyncQuery::saveGeometry);
244+ connect(this, &WindowStateStorage::saveStage, m_asyncQuery, &AsyncQuery::saveStage);
245+ m_asyncOk = true;
107246 }
108− return query.value("stage").toInt();
109247}
110248
111−QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
249+WindowStateStorage::~WindowStateStorage()
112250{
113− QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
114− .arg(sanitiseString(windowId));
251+ m_thread.quit();
252+ m_thread.wait();
253+}
115254
116− QSqlQuery query = getValue(queryString);
255+void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState windowState)
256+{
257+ if (!m_asyncOk) {
258+ return;
259+ }
260+ // This could be a simple connection like all the other save* methods
261+ // on AsyncQuery, but WindowStateStorage::WindowState can't be used
262+ // on it directly.
263+ QMetaObject::invokeMethod(m_asyncQuery, "saveState", Qt::QueuedConnection,
264+ Q_ARG(const QString&, windowId),
265+ Q_ARG(int, (int)windowState)
266+ );
267+}
117268
118− if (!query.first()) {
269+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
270+{
271+ if (!m_asyncOk) {
119272 return defaultValue;
120273 }
274+ int state;
121275
122− const QRect result(query.value(QStringLiteral("x")).toInt(), query.value(QStringLiteral("y")).toInt(),
123− query.value(QStringLiteral("width")).toInt(), query.value(QStringLiteral("height")).toInt());
276+ QMetaObject::invokeMethod(m_asyncQuery, "getState", Qt::BlockingQueuedConnection,
277+ Q_RETURN_ARG(int, state),
278+ Q_ARG(const QString&, windowId)
279+ );
124280
125− if (result.isValid()) {
126− return result;
281+ if (state == -1) {
282+ return defaultValue;
127283 }
128284
129− return defaultValue;
285+ return (WindowState)state;
130286}
131287
132−void WindowStateStorage::initdb()
288+int WindowStateStorage::getStage(const QString &appId, int defaultValue) const
133289{
134− m_db.open();
135− if (!m_db.open()) {
136− qWarning() << "Error opening state database:" << m_db.lastError().driverText() << m_db.lastError().databaseText();
137− return;
290+ if (!m_asyncOk) {
291+ return defaultValue;
138292 }
293+ int stage;
139294
140− if (!m_db.tables().contains(QStringLiteral("geometry"))) {
141− QSqlQuery query;
142− query.exec(QStringLiteral("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);"));
143− }
295+ QMetaObject::invokeMethod(m_asyncQuery, "getStage", Qt::BlockingQueuedConnection,
296+ Q_RETURN_ARG(int, stage),
297+ Q_ARG(const QString&, appId)
298+ );
144299
145− if (!m_db.tables().contains(QStringLiteral("state"))) {
146− QSqlQuery query;
147− query.exec(QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);"));
300+ if (stage == -1) {
301+ return defaultValue;
148302 }
149303
150− if (!m_db.tables().contains(QStringLiteral("stage"))) {
151− QSqlQuery query;
152− query.exec(QStringLiteral("CREATE TABLE stage(appId TEXT UNIQUE, stage INTEGER);"));
153− }
304+ return stage;
154305}
155306
156−void WindowStateStorage::saveValue(const QString &queryString)
307+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
157308{
158− QMutexLocker mutexLocker(&s_mutex);
159− QSqlQuery query;
160− auto ok = query.exec(queryString);
161− if (!ok) {
162− qWarning() << "Error executing query" << queryString
163− << "Driver error:" << query.lastError().driverText()
164− << "Database error:" << query.lastError().databaseText();
309+ if (!m_asyncOk) {
310+ return defaultValue;
165311 }
312+ QRect geometry;
313+ QMetaObject::invokeMethod(m_asyncQuery, "getGeometry", Qt::BlockingQueuedConnection,
314+ Q_RETURN_ARG(QRect, geometry),
315+ Q_ARG(const QString&, windowId)
316+ );
317+ if (geometry.isNull() || !geometry.isValid()) {
318+ return defaultValue;
319+ }
320+ return geometry;
166321}
167322
168−QSqlQuery WindowStateStorage::getValue(const QString &queryString) const
323+const QString WindowStateStorage::getDbName()
169324{
170− QMutexLocker l(&s_mutex);
171− QSqlQuery query;
172−
173− bool ok = query.exec(queryString);
174− if (!ok) {
175− qWarning() << "Error retrieving database query:" << queryString
176− << "Driver error:" << query.lastError().driverText()
177− << "Database error:" << query.lastError().databaseText();
325+ if (!m_asyncOk) {
326+ return QStringLiteral("ERROR");
178327 }
179− return query;
328+ QString dbName;
329+ QMetaObject::invokeMethod(m_asyncQuery, "getDbName", Qt::BlockingQueuedConnection,
330+ Q_RETURN_ARG(QString, dbName)
331+ );
332+ return QString(dbName);
180333}
181334
182335Mir::State WindowStateStorage::toMirState(WindowState state) const
@@ -201,3 +354,5 @@ Mir::State WindowStateStorage::toMirState(WindowState state) const
201354 return Mir::RestoredState;
202355 }
203356}
357+
358+#include "windowstatestorage.moc"
plugins/Utils/windowstatestorage.h+47−10
@@ -1,5 +1,6 @@
11/*
22 * Copyright 2015-2016 Canonical Ltd.
3+ * Copyright 2021 UBports Foundation
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU Lesser General Public License as published by
@@ -16,11 +17,47 @@
1617
1718#include <QObject>
1819#include <QSqlDatabase>
20+#include <QSqlQuery>
1921#include <QMutex>
22+#include <QFuture>
23+#include <QThread>
2024
2125// unity-api
2226#include <unity/shell/application/Mir.h>
2327
28+class AsyncQuery: public QObject
29+{
30+ Q_OBJECT
31+
32+public:
33+ AsyncQuery(const QString& dbName);
34+ ~AsyncQuery();
35+
36+ Q_PROPERTY (const QString dbName READ getDbName)
37+
38+ Q_INVOKABLE const QString getDbName();
39+ Q_INVOKABLE bool initdb();
40+ Q_INVOKABLE int getState(const QString &windowId) const;
41+ Q_INVOKABLE QRect getGeometry(const QString &windowId) const;
42+ Q_INVOKABLE int getStage(const QString &appId) const;
43+
44+public Q_SLOTS:
45+ void saveState(const QString &windowId, int state);
46+ void saveGeometry(const QString &windowId, const QRect &rect);
47+ void saveStage(const QString &appId, int stage);
48+
49+private:
50+ const QString m_connectionName = QStringLiteral("WindowStateStorage");
51+ const QString m_getStateQuery = QStringLiteral("SELECT state FROM state WHERE windowId = :windowId");
52+ const QString m_saveStateQuery = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values (:windowId, :state)");
53+ const QString m_getGeometryQuery = QStringLiteral("SELECT * FROM geometry WHERE windowId = :windowId");
54+ const QString m_saveGeometryQuery = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values (:windowId, :x, :y, :width, :height)");
55+ const QString m_getStageQuery = QStringLiteral("SELECT stage FROM stage WHERE appId = :appId");
56+ const QString m_saveStageQuery = QStringLiteral("INSERT OR REPLACE INTO stage (appId, stage) values (:appId, :stage)");
57+ void logSqlError(const QSqlQuery) const;
58+ QString m_dbName;
59+};
60+
2461class WindowStateStorage: public QObject
2562{
2663 Q_OBJECT
@@ -47,25 +84,25 @@ public:
4784 WindowStateStorage(const QString& dbName = nullptr, QObject *parent = nullptr);
4885 virtual ~WindowStateStorage();
4986
50− Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
87+ Q_INVOKABLE void saveState(const QString &windowId, WindowStateStorage::WindowState state);
5188 Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
5289
53− Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
5490 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
5591
56− Q_INVOKABLE void saveStage(const QString &appId, int stage);
5792 Q_INVOKABLE int getStage(const QString &appId, int defaultValue) const;
5893
5994 Q_INVOKABLE Mir::State toMirState(WindowState state) const;
6095
61−private:
62− void initdb();
96+ Q_PROPERTY (const QString dbName READ getDbName)
6397
64− void saveValue(const QString &queryString);
65− QSqlQuery getValue(const QString &queryString) const;
98+ const QString getDbName();
6699
67− static QMutex s_mutex;
100+Q_SIGNALS:
101+ void saveStage(const QString &appId, int stage);
102+ void saveGeometry(const QString &windowId, const QRect &rect);
68103
69− // NB: This is accessed from threads. Make sure to mutex it.
70− QSqlDatabase m_db;
104+private:
105+ QThread m_thread;
106+ AsyncQuery *m_asyncQuery;
107+ bool m_asyncOk;
71108};
tests/plugins/Utils/WindowStateStorageTest.cpp+47
@@ -38,6 +38,38 @@ private Q_SLOTS:
3838 delete storage;
3939 }
4040
41+ void testDbNameMemory() {
42+ QCOMPARE(storage->getDbName(), QStringLiteral(":memory:"));
43+ }
44+
45+ // This test serves as a reminder: if you're changing the databse location,
46+ // copy the old database or risk the wrath of long-time users.
47+ void testDbNameDefault() {
48+ delete storage;
49+ storage = new WindowStateStorage(nullptr, this);
50+ QCOMPARE(storage->getDbName(), QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/windowstatestorage.sqlite"));
51+ }
52+
53+ // Ensure that the WindowStateStorage object can be used even if opening
54+ // the database fails, just returning default values.
55+ void testErrorBehavior() {
56+ QString id{QTest::currentTestFunction()};
57+ QRect defaultValue{1, 1, 1, 1};
58+ WindowStateStorage::WindowState state{WindowStateStorage::WindowState::WindowStateMaximizedTopLeft};
59+ WindowStateStorage::WindowState defaultState{WindowStateStorage::WindowState::WindowStateMaximizedBottomRight};
60+
61+ delete storage;
62+ storage = new WindowStateStorage(QStringLiteral("/nonexistent/there/is/no/way/this/exists/"), this);
63+ QCOMPARE(storage->getDbName(), QStringLiteral("ERROR"));
64+
65+ storage->saveState(id, state);
66+ storage->saveGeometry(id, QRect(2, 2, 2, 2));
67+ storage->saveStage(id, 2);
68+ QCOMPARE(storage->getState(id, defaultState), defaultState);
69+ QCOMPARE(storage->getGeometry(id, defaultValue), defaultValue);
70+ QCOMPARE(storage->getStage(id, 4), 4);
71+ }
72+
4173 void testSaveRestoreState() {
4274 const WindowStateStorage::WindowState state{WindowStateStorage::WindowState::WindowStateMaximizedTopLeft};
4375 storage->saveState(QTest::currentTestFunction(), state);
@@ -65,6 +97,21 @@ private Q_SLOTS:
6597 QCOMPARE(loadedGeometry, defaultGeometry);
6698 }
6799
100+ void testProtectAgainstSqlInjection() {
101+ QString naughtyAppId = QStringLiteral("stageQuery;DROP TABLE stage");
102+ QString naughtyAppIdSingleQuotes = QStringLiteral("stageQuery';DROP TABLE stage");
103+ QString naughtyAppIdDoubleQuotes = QStringLiteral("stageQuery\";DROP TABLE stage");
104+ QString naughtyAppIdBackslashes = QStringLiteral("stageQuery\\\";DROP TABLE stage");
105+ storage->saveStage(naughtyAppId, 1);
106+ QTRY_COMPARE(storage->getStage(naughtyAppId, 0), 1);
107+ storage->saveStage(naughtyAppIdSingleQuotes, 2);
108+ QTRY_COMPARE(storage->getStage(naughtyAppIdSingleQuotes, 0), 2);
109+ storage->saveStage(naughtyAppIdDoubleQuotes, 3);
110+ QTRY_COMPARE(storage->getStage(naughtyAppIdDoubleQuotes, 0), 3);
111+ storage->saveStage(naughtyAppIdBackslashes, 4);
112+ QTRY_COMPARE(storage->getStage(naughtyAppIdBackslashes, 0), 4);
113+ }
114+
68115private:
69116 WindowStateStorage * storage{nullptr};
70117};