on xenial help sign in
Rewrite WindowStateStorage for improved async power
add6528Dalton Durst
plugins/Utils/windowstatestorage.cpp+253−98
@@ -1,5 +1,6 @@
1/*
1/*
2 * Copyright 2015-2016 Canonical Ltd.
2 * Copyright 2015-2016 Canonical Ltd.
3+ * Copyright 2021 UBports Foundation
3 *
4 *
4 * This program is free software; you can redistribute it and/or modify
5 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * it under the terms of the GNU Lesser General Public License as published by
@@ -16,167 +17,319 @@
16
17
17#include "windowstatestorage.h"
18#include "windowstatestorage.h"
18
19
19−#include <QtConcurrent>
20#include <QDebug>
20#include <QDebug>
21+#include <QDir>
22+#include <QMetaObject>
23+#include <QObject>
21#include <QSqlQuery>
24#include <QSqlQuery>
22#include <QSqlError>
25#include <QSqlError>
23#include <QSqlResult>
26#include <QSqlResult>
27+#include <QStandardPaths>
24#include <QRect>
28#include <QRect>
25#include <unity/shell/application/ApplicationInfoInterface.h>
29#include <unity/shell/application/ApplicationInfoInterface.h>
26
30
27−QMutex WindowStateStorage::s_mutex;
31+AsyncQuery::AsyncQuery(const QString& dbName):
32+ m_dbName(dbName)
33+{
34+}
28
35
29−inline QString sanitiseString(QString string) {
36+AsyncQuery::~AsyncQuery()
30− return string.remove(QLatin1Char('\"'))
37+{
31− .remove(QLatin1Char('\''))
38+ QSqlDatabase::removeDatabase(m_connectionName);
32− .remove(QLatin1Char('\\'));
33}
39}
34
40
35−WindowStateStorage::WindowStateStorage(const QString& dbName, QObject *parent):
41+bool AsyncQuery::initdb()
36− QObject(parent)
37−{
42+{
38− const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
39− m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"));
43+ QSqlDatabase connection = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
40− QDir dir;
44+ connection.setDatabaseName(m_dbName);
41− dir.mkpath(dbPath);
45+ if (!connection.open()) {
42− if (dbName != nullptr) {
46+ qWarning() << "Error opening state database " << m_dbName << connection.lastError().driverText() << connection.lastError().databaseText();
43− m_db.setDatabaseName(dbName);
47+ return false;
44− } else {
48+ }
45− m_db.setDatabaseName(dbPath + "windowstatestorage.sqlite");
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+ }
46 }
57 }
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;
48}
75}
49
76
50−WindowStateStorage::~WindowStateStorage()
77+void AsyncQuery::saveState(const QString &windowId, int state)
51{
78{
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+ }
53}
87}
54
88
55−void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
89+int AsyncQuery::getState(const QString &windowId) const
56{
90{
57− const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
91+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
58− .arg(sanitiseString(windowId))
92+ QSqlQuery query(connection);
59− .arg((int)state);
93+ query.prepare(m_getStateQuery);
60−
94+ query.bindValue(":windowId", windowId);
61− saveValue(queryString);
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+ }
62}
112}
63
113
64−WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
114+void AsyncQuery::saveGeometry(const QString &windowId, const QRect &rect)
65{
115{
66− const QString queryString = QStringLiteral("SELECT state FROM state WHERE windowId = '%1';")
116+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
67− .arg(sanitiseString(windowId));
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+}
68
128
129+QRect AsyncQuery::getGeometry(const QString &windowId) const
130+{
131+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
69− QSqlQuery query = getValue(queryString);
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+ }
70
140
71 if (!query.first()) {
141 if (!query.first()) {
72− return defaultValue;
142+ return QRect();
73 }
143 }
74− return (WindowState)query.value(QStringLiteral("state")).toInt();
75−}
76
144
77−void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
145+ bool xConverted, yConverted, widthConverted, heightConverted = false;
78−{
146+ int x, y, width, height;
79− const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
147+ QVariant xResultStr = query.value(QStringLiteral("x"));
80− .arg(sanitiseString(windowId))
148+ QVariant yResultStr = query.value(QStringLiteral("y"));
81− .arg(rect.x())
149+ QVariant widthResultStr = query.value(QStringLiteral("width"));
82− .arg(rect.y())
150+ QVariant heightResultStr = query.value(QStringLiteral("height"));
83− .arg(rect.width())
151+ x = xResultStr.toInt(&xConverted);
84− .arg(rect.height());
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+ }
85
164
86− saveValue(queryString);
87}
165}
88
166
89−void WindowStateStorage::saveStage(const QString &appId, int stage)
167+void AsyncQuery::saveStage(const QString &appId, int stage)
90{
168{
91− const QString queryString = QStringLiteral("INSERT OR REPLACE INTO stage (appId, stage) values ('%1', '%2');")
169+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
92− .arg(sanitiseString(appId))
170+ QSqlQuery query(connection);
93− .arg(stage);
171+ query.prepare(m_saveStageQuery);
172+ query.bindValue(":appId", appId);
173+ query.bindValue(":stage", stage);
174+ if (!query.exec()) {
175+ logSqlError(query);
176+ }
177+}
94
178
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+ }
96}
202}
97
203
98−int WindowStateStorage::getStage(const QString &appId, int defaultValue) const
204+void AsyncQuery::logSqlError(const QSqlQuery query) const
99{
205{
100− const QString queryString = QStringLiteral("SELECT stage FROM stage WHERE appId = '%1';")
206+ qWarning() << "Error executing query" << query.lastQuery()
101− .arg(sanitiseString(appId));
207+ << "Driver error:" << query.lastError().driverText()
208+ << "Database error:" << query.lastError().databaseText();
209+}
102
210
103− QSqlQuery query = getValue(queryString);
211+const QString AsyncQuery::getDbName()
212+{
213+ QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
214+ return connection.databaseName();
215+}
104
216
105− if (!query.first()) {
217+WindowStateStorage::WindowStateStorage(const QString& dbName, QObject *parent):
106− return defaultValue;
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;
107 }
246 }
108− return query.value("stage").toInt();
109}
247}
110
248
111−QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
249+WindowStateStorage::~WindowStateStorage()
112{
250{
113− QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
251+ m_thread.quit();
114− .arg(sanitiseString(windowId));
252+ m_thread.wait();
253+}
115
254
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+}
117
268
118− if (!query.first()) {
269+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
270+{
271+ if (!m_asyncOk) {
119 return defaultValue;
272 return defaultValue;
120 }
273 }
274+ int state;
121
275
122− const QRect result(query.value(QStringLiteral("x")).toInt(), query.value(QStringLiteral("y")).toInt(),
276+ QMetaObject::invokeMethod(m_asyncQuery, "getState", Qt::BlockingQueuedConnection,
123− query.value(QStringLiteral("width")).toInt(), query.value(QStringLiteral("height")).toInt());
277+ Q_RETURN_ARG(int, state),
278+ Q_ARG(const QString&, windowId)
279+ );
124
280
125− if (result.isValid()) {
281+ if (state == -1) {
126− return result;
282+ return defaultValue;
127 }
283 }
128
284
129− return defaultValue;
285+ return (WindowState)state;
130}
286}
131
287
132−void WindowStateStorage::initdb()
288+int WindowStateStorage::getStage(const QString &appId, int defaultValue) const
133{
289{
134− m_db.open();
290+ if (!m_asyncOk) {
135− if (!m_db.open()) {
136− qWarning() << "Error opening state database:" << m_db.lastError().driverText() << m_db.lastError().databaseText();
137− return;
291+ return defaultValue;
138 }
292 }
293+ int stage;
139
294
140− if (!m_db.tables().contains(QStringLiteral("geometry"))) {
295+ QMetaObject::invokeMethod(m_asyncQuery, "getStage", Qt::BlockingQueuedConnection,
141− QSqlQuery query;
296+ Q_RETURN_ARG(int, stage),
142− query.exec(QStringLiteral("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);"));
297+ Q_ARG(const QString&, appId)
143− }
298+ );
144
299
145− if (!m_db.tables().contains(QStringLiteral("state"))) {
300+ if (stage == -1) {
146− QSqlQuery query;
301+ return defaultValue;
147− query.exec(QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);"));
148 }
302 }
149
303
150− if (!m_db.tables().contains(QStringLiteral("stage"))) {
304+ return stage;
151− QSqlQuery query;
152− query.exec(QStringLiteral("CREATE TABLE stage(appId TEXT UNIQUE, stage INTEGER);"));
153− }
154}
305}
155
306
156−void WindowStateStorage::saveValue(const QString &queryString)
307+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
157{
308{
158− QMutexLocker mutexLocker(&s_mutex);
309+ if (!m_asyncOk) {
159− QSqlQuery query;
310+ return defaultValue;
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();
165 }
311 }
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;
166}
321}
167
322
168−QSqlQuery WindowStateStorage::getValue(const QString &queryString) const
323+const QString WindowStateStorage::getDbName()
169{
324{
170− QMutexLocker l(&s_mutex);
325+ if (!m_asyncOk) {
171− QSqlQuery query;
326+ return QStringLiteral("ERROR");
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();
178 }
327 }
328+ QString dbName;
329+ QMetaObject::invokeMethod(m_asyncQuery, "getDbName", Qt::BlockingQueuedConnection,
330+ Q_RETURN_ARG(QString, dbName)
331+ );
179− return query;
332+ return QString(dbName);
180}
333}
181
334
182Mir::State WindowStateStorage::toMirState(WindowState state) const
335Mir::State WindowStateStorage::toMirState(WindowState state) const
@@ -201,3 +354,5 @@ Mir::State WindowStateStorage::toMirState(WindowState state) const
201 return Mir::RestoredState;
354 return Mir::RestoredState;
202 }
355 }
203}
356}
357+
358+#include "windowstatestorage.moc"
plugins/Utils/windowstatestorage.h+47−10
@@ -1,5 +1,6 @@
1/*
1/*
2 * Copyright 2015-2016 Canonical Ltd.
2 * Copyright 2015-2016 Canonical Ltd.
3+ * Copyright 2021 UBports Foundation
3 *
4 *
4 * This program is free software; you can redistribute it and/or modify
5 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * it under the terms of the GNU Lesser General Public License as published by
@@ -16,11 +17,47 @@
16
17
17#include <QObject>
18#include <QObject>
18#include <QSqlDatabase>
19#include <QSqlDatabase>
20+#include <QSqlQuery>
19#include <QMutex>
21#include <QMutex>
22+#include <QFuture>
23+#include <QThread>
20
24
21// unity-api
25// unity-api
22#include <unity/shell/application/Mir.h>
26#include <unity/shell/application/Mir.h>
23
27
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+
24class WindowStateStorage: public QObject
61class WindowStateStorage: public QObject
25{
62{
26 Q_OBJECT
63 Q_OBJECT
@@ -47,25 +84,25 @@ public:
47 WindowStateStorage(const QString& dbName = nullptr, QObject *parent = nullptr);
84 WindowStateStorage(const QString& dbName = nullptr, QObject *parent = nullptr);
48 virtual ~WindowStateStorage();
85 virtual ~WindowStateStorage();
49
86
50− Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
87+ Q_INVOKABLE void saveState(const QString &windowId, WindowStateStorage::WindowState state);
51 Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
88 Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
52
89
53− Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
54 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
90 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
55
91
56− Q_INVOKABLE void saveStage(const QString &appId, int stage);
57 Q_INVOKABLE int getStage(const QString &appId, int defaultValue) const;
92 Q_INVOKABLE int getStage(const QString &appId, int defaultValue) const;
58
93
59 Q_INVOKABLE Mir::State toMirState(WindowState state) const;
94 Q_INVOKABLE Mir::State toMirState(WindowState state) const;
60
95
61−private:
96+ Q_PROPERTY (const QString dbName READ getDbName)
62− void initdb();
63
97
64− void saveValue(const QString &queryString);
98+ const QString getDbName();
65− QSqlQuery getValue(const QString &queryString) const;
66
99
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);
68
103
69− // NB: This is accessed from threads. Make sure to mutex it.
104+private:
70− QSqlDatabase m_db;
105+ QThread m_thread;
106+ AsyncQuery *m_asyncQuery;
107+ bool m_asyncOk;
71};
108};
tests/plugins/Utils/WindowStateStorageTest.cpp+47
@@ -38,6 +38,38 @@ private Q_SLOTS:
38 delete storage;
38 delete storage;
39 }
39 }
40
40
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+
41 void testSaveRestoreState() {
73 void testSaveRestoreState() {
42 const WindowStateStorage::WindowState state{WindowStateStorage::WindowState::WindowStateMaximizedTopLeft};
74 const WindowStateStorage::WindowState state{WindowStateStorage::WindowState::WindowStateMaximizedTopLeft};
43 storage->saveState(QTest::currentTestFunction(), state);
75 storage->saveState(QTest::currentTestFunction(), state);
@@ -65,6 +97,21 @@ private Q_SLOTS:
65 QCOMPARE(loadedGeometry, defaultGeometry);
97 QCOMPARE(loadedGeometry, defaultGeometry);
66 }
98 }
67
99
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+
68private:
115private:
69 WindowStateStorage * storage{nullptr};
116 WindowStateStorage * storage{nullptr};
70};
117};