Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qsettings_wasm.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:trusted-data-only
4
5#include "qsettings.h"
6#ifndef QT_NO_SETTINGS
7
8#include "qsettings_p.h"
9#ifndef QT_NO_QOBJECT
11#include <QFile>
12#endif // QT_NO_QOBJECT
13#include <QDebug>
14#include <QtCore/private/qstdweb_p.h>
15#include <QtCore/private/qwasmglobal_p.h>
16
17#include <QFileInfo>
18#include <QDir>
19#include <QList>
20#include <QSet>
21
22#include <emscripten.h>
23# include <emscripten/proxying.h>
24# include <emscripten/threading.h>
25# include <emscripten/val.h>
26
28
29using emscripten::val;
30using namespace Qt::StringLiterals;
31
32namespace {
33QStringView keyNameFromPrefixedStorageName(QStringView prefix, QStringView prefixedStorageName)
34{
35 // Return the key slice after m_keyPrefix, or an empty string view if no match
36 if (!prefixedStorageName.startsWith(prefix))
37 return QStringView();
38 return prefixedStorageName.sliced(prefix.length());
39}
40} // namespace
41
42//
43// Native settings implementation for WebAssembly using window.localStorage
44// as the storage backend. localStorage is a key-value store with a synchronous
45// API and a 5MB storage limit.
46//
47class QWasmLocalStorageSettingsPrivate final : public QSettingsPrivate
48{
49public:
51 const QString &application);
53
54 void remove(const QString &key) final;
55 void set(const QString &key, const QVariant &value) final;
56 std::optional<QVariant> get(const QString &key) const final;
57 QStringList children(const QString &prefix, ChildSpec spec) const final;
58 void clear() final;
59 void sync() final;
60 void flush() final;
61 bool isWritable() const final;
62 QString fileName() const final;
63
64private:
65 QStringList m_keyPrefixes;
66};
67
68QWasmLocalStorageSettingsPrivate::QWasmLocalStorageSettingsPrivate(QSettings::Scope scope,
69 const QString &organization,
70 const QString &application)
71 : QSettingsPrivate(QSettings::NativeFormat, scope, organization, application)
72{
73 if (organization.isEmpty()) {
74 setStatus(QSettings::AccessError);
75 return;
76 }
77
78 // The key prefix contians "qt" to separate Qt keys from other keys on localStorage, a
79 // version tag to allow for making changes to the key format in the future, the org
80 // and app names.
81 //
82 // User code could could create separate settings object with different org and app names,
83 // and would expect them to have separate settings. Also, different webassembly instances
84 // on the page could write to the same window.localStorage. Add the org and app name
85 // to the key prefix to differentiate, even if that leads to keys with redundant sections
86 // for the common case of a single org and app name.
87 //
88 // Also, the common Qt mechanism for user/system scope and all-application settings are
89 // implemented, using different prefixes.
90 const QString allAppsSetting = QStringLiteral("all-apps");
91 const QString systemSetting = QStringLiteral("sys-tem");
92
93 const QLatin1String separator("-");
94 const QLatin1String doubleSeparator("--");
95 const QString escapedOrganization = QString(organization).replace(separator, doubleSeparator);
96 const QString escapedApplication = QString(application).replace(separator, doubleSeparator);
97 const QString prefix = "qt-v0-" + escapedOrganization + separator;
98 if (scope == QSettings::Scope::UserScope) {
99 if (!escapedApplication.isEmpty())
100 m_keyPrefixes.push_back(prefix + escapedApplication + separator);
101 m_keyPrefixes.push_back(prefix + allAppsSetting + separator);
102 }
103 if (!escapedApplication.isEmpty()) {
104 m_keyPrefixes.push_back(prefix + escapedApplication + separator + systemSetting
105 + separator);
106 }
107 m_keyPrefixes.push_back(prefix + allAppsSetting + separator + systemSetting + separator);
108}
109
110void QWasmLocalStorageSettingsPrivate::remove(const QString &key)
111{
112 const std::string removed = QString(m_keyPrefixes.first() + key).toStdString();
113
114 qwasmglobal::runTaskOnMainThread<void>([this, &removed, &key]() {
115 std::vector<std::string> children = { removed };
116 const int length = val::global("window")["localStorage"]["length"].as<int>();
117 for (int i = 0; i < length; ++i) {
118 const QString storedKeyWithPrefix = QString::fromStdString(
119 val::global("window")["localStorage"].call<val>("key", i).as<std::string>());
120
121 const QStringView storedKey = keyNameFromPrefixedStorageName(
122 m_keyPrefixes.first(), QStringView(storedKeyWithPrefix));
123 if (storedKey.isEmpty() || !storedKey.startsWith(key))
124 continue;
125
126 children.push_back(storedKeyWithPrefix.toStdString());
127 }
128
129 for (const auto &child : children)
130 val::global("window")["localStorage"].call<val>("removeItem", child);
131 });
132}
133
134void QWasmLocalStorageSettingsPrivate::set(const QString &key, const QVariant &value)
135{
136 qwasmglobal::runTaskOnMainThread<void>([this, &key, &value]() {
137 const std::string keyString = QString(m_keyPrefixes.first() + key).toStdString();
138 const std::string valueString = QSettingsPrivate::variantToString(value).toStdString();
139 val::global("window")["localStorage"].call<void>("setItem", keyString, valueString);
140 });
141}
142
143std::optional<QVariant> QWasmLocalStorageSettingsPrivate::get(const QString &key) const
144{
145 return qwasmglobal::runTaskOnMainThread<std::optional<QVariant>>(
146 [this, &key]() -> std::optional<QVariant> {
147 for (const auto &prefix : m_keyPrefixes) {
148 const std::string keyString = QString(prefix + key).toStdString();
149 const emscripten::val value =
150 val::global("window")["localStorage"].call<val>("getItem", keyString);
151 if (!value.isNull()) {
152 return QSettingsPrivate::stringToVariant(
153 QString::fromStdString(value.as<std::string>()));
154 }
155 if (!fallbacks) {
156 return std::nullopt;
157 }
158 }
159 return std::nullopt;
160 });
161}
162
163QStringList QWasmLocalStorageSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
164{
165 return qwasmglobal::runTaskOnMainThread<QStringList>([this, &prefix, &spec]() -> QStringList {
166 QSet<QString> nodes;
167 // Loop through all keys on window.localStorage, return Qt keys belonging to
168 // this application, with the correct prefix, and according to ChildSpec.
169 QStringList children;
170 const int length = val::global("window")["localStorage"]["length"].as<int>();
171 for (int i = 0; i < length; ++i) {
172 for (const auto &storagePrefix : m_keyPrefixes) {
173 const QString keyString =
174 QString::fromStdString(val::global("window")["localStorage"]
175 .call<val>("key", i)
176 .as<std::string>());
177
178 const QStringView key =
179 keyNameFromPrefixedStorageName(storagePrefix, QStringView(keyString));
180 if (!key.isEmpty() && key.startsWith(prefix)) {
181 QStringList children;
182 QSettingsPrivate::processChild(key.sliced(prefix.length()), spec, children);
183 if (!children.isEmpty())
184 nodes.insert(children.first());
185 }
186 if (!fallbacks)
187 break;
188 }
189 }
190
191 return QStringList(nodes.begin(), nodes.end());
192 });
193}
194
195void QWasmLocalStorageSettingsPrivate::clear()
196{
197 qwasmglobal::runTaskOnMainThread<void>([this]() {
198 // Get all Qt keys from window.localStorage
199 const int length = val::global("window")["localStorage"]["length"].as<int>();
200 QStringList keys;
201 keys.reserve(length);
202 for (int i = 0; i < length; ++i)
203 keys.append(QString::fromStdString(
204 (val::global("window")["localStorage"].call<val>("key", i).as<std::string>())));
205
206 // Remove all Qt keys. Note that localStorage does not guarantee a stable
207 // iteration order when the storage is mutated, which is why removal is done
208 // in a second step after getting all keys.
209 for (const QString &key : keys) {
210 if (!keyNameFromPrefixedStorageName(m_keyPrefixes.first(), key).isEmpty())
211 val::global("window")["localStorage"].call<val>("removeItem", key.toStdString());
212 }
213 });
214}
215
216void QWasmLocalStorageSettingsPrivate::sync() { }
217
218void QWasmLocalStorageSettingsPrivate::flush() { }
219
220bool QWasmLocalStorageSettingsPrivate::isWritable() const
221{
222 return true;
223}
224
225QString QWasmLocalStorageSettingsPrivate::fileName() const
226{
227 return QString();
228}
229
230//
231// Native settings implementation for WebAssembly using the indexed database as
232// the storage backend
233//
235{
236public:
238 const QString &application);
240
243
244private:
245 bool writeSettingsToTemporaryFile(const QString &fileName, void *dataPtr, int size);
246 void loadIndexedDBFiles();
247
248
249 QString databaseName;
250 QString id;
251};
252
253constexpr char DbName[] = "/home/web_user";
254
255QWasmIDBSettingsPrivate::QWasmIDBSettingsPrivate(QSettings::Scope scope,
256 const QString &organization,
257 const QString &application)
258 : QConfFileSettingsPrivate(QSettings::WebIndexedDBFormat, scope, organization, application)
259{
260 Q_ASSERT_X(qstdweb::haveJspi(), Q_FUNC_INFO, "QWasmIDBSettingsPrivate needs JSPI to work");
261
262 if (organization.isEmpty()) {
263 setStatus(QSettings::AccessError);
264 return;
265 }
266
267 databaseName = organization;
268 id = application;
269
270 loadIndexedDBFiles();
271
273}
274
276
277bool QWasmIDBSettingsPrivate::writeSettingsToTemporaryFile(const QString &fileName, void *dataPtr,
278 int size)
279{
280 QFile file(fileName);
281 QFileInfo fileInfo(fileName);
282 QDir dir(fileInfo.path());
283 if (!dir.exists())
284 dir.mkpath(fileInfo.path());
285
286 if (!file.open(QFile::WriteOnly))
287 return false;
288
289 return size == file.write(reinterpret_cast<char *>(dataPtr), size);
290}
291
293{
295
296 int error = 0;
297 emscripten_idb_delete(DbName, fileName().toLocal8Bit(), &error);
298 setStatus(!!error ? QSettings::AccessError : QSettings::NoError);
299}
300
302{
303 // Reload the files, in case there were any changes in IndexedDB, and flush them to disk.
304 // Thanks to this, QConfFileSettingsPrivate::sync will handle key merging correctly.
305 loadIndexedDBFiles();
306
308
309 QFile file(fileName());
310 if (file.open(QFile::ReadOnly)) {
311 QByteArray dataPointer = file.readAll();
312
313 int error = 0;
314 emscripten_idb_store(DbName, fileName().toLocal8Bit(),
315 reinterpret_cast<void *>(dataPointer.data()), dataPointer.length(),
316 &error);
317 setStatus(!!error ? QSettings::AccessError : QSettings::NoError);
318 }
319}
320
321void QWasmIDBSettingsPrivate::loadIndexedDBFiles()
322{
323 for (const auto *confFile : getConfFiles()) {
324 int exists = 0;
325 int error = 0;
326 emscripten_idb_exists(DbName, confFile->name.toLocal8Bit(), &exists, &error);
327 if (error) {
328 setStatus(QSettings::AccessError);
329 return;
330 }
331 if (exists) {
332 void *contents;
333 int size;
334 emscripten_idb_load(DbName, confFile->name.toLocal8Bit(), &contents, &size, &error);
335 if (error || !writeSettingsToTemporaryFile(confFile->name, contents, size)) {
336 setStatus(QSettings::AccessError);
337 return;
338 }
339 }
340 }
341}
342
343QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format, QSettings::Scope scope,
344 const QString &organization, const QString &application)
345{
346 // Make WebLocalStorageFormat the default native format
347 if (format == QSettings::NativeFormat)
348 format = QSettings::WebLocalStorageFormat;
349
350 // Check if cookies are enabled (required for using persistent storage)
351
352 const bool cookiesEnabled = qwasmglobal::runTaskOnMainThread<bool>(
353 []() { return val::global("navigator")["cookieEnabled"].as<bool>(); });
354
355 constexpr QLatin1StringView cookiesWarningMessage(
356 "QSettings::%1 requires cookies, falling back to IniFormat with temporary file");
357 if (!cookiesEnabled) {
358 if (format == QSettings::WebLocalStorageFormat) {
359 qWarning() << cookiesWarningMessage.arg("WebLocalStorageFormat");
360 format = QSettings::IniFormat;
361 } else if (format == QSettings::WebIndexedDBFormat) {
362 qWarning() << cookiesWarningMessage.arg("WebIndexedDBFormat");
363 format = QSettings::IniFormat;
364 }
365 }
366 if (format == QSettings::WebIndexedDBFormat && !qstdweb::haveJspi()) {
367 qWarning() << "QSettings::WebIndexedDBFormat requires JSPI, falling back to IniFormat with "
368 "temporary file";
369 format = QSettings::IniFormat;
370 }
371
372 // Create settings backend according to selected format
373 switch (format) {
374 case QSettings::Format::WebLocalStorageFormat:
375 return new QWasmLocalStorageSettingsPrivate(scope, organization, application);
376 case QSettings::Format::WebIndexedDBFormat:
377 return new QWasmIDBSettingsPrivate(scope, organization, application);
378 case QSettings::Format::IniFormat:
379 case QSettings::Format::CustomFormat1:
380 case QSettings::Format::CustomFormat2:
381 case QSettings::Format::CustomFormat3:
382 case QSettings::Format::CustomFormat4:
383 case QSettings::Format::CustomFormat5:
384 case QSettings::Format::CustomFormat6:
385 case QSettings::Format::CustomFormat7:
386 case QSettings::Format::CustomFormat8:
387 case QSettings::Format::CustomFormat9:
388 case QSettings::Format::CustomFormat10:
389 case QSettings::Format::CustomFormat11:
390 case QSettings::Format::CustomFormat12:
391 case QSettings::Format::CustomFormat13:
392 case QSettings::Format::CustomFormat14:
393 case QSettings::Format::CustomFormat15:
394 case QSettings::Format::CustomFormat16:
395 return new QConfFileSettingsPrivate(format, scope, organization, application);
396 case QSettings::Format::InvalidFormat:
397 return nullptr;
398 case QSettings::Format::NativeFormat:
399 Q_UNREACHABLE();
400 break;
401 }
402}
403
404QT_END_NAMESPACE
405#endif // QT_NO_SETTINGS
virtual void initAccess()
QWasmIDBSettingsPrivate(QSettings::Scope scope, const QString &organization, const QString &application)
std::optional< QVariant > get(const QString &key) const final
~QWasmLocalStorageSettingsPrivate() final=default
QWasmLocalStorageSettingsPrivate(QSettings::Scope scope, const QString &organization, const QString &application)
QStringList children(const QString &prefix, ChildSpec spec) const final
void remove(const QString &key) final
void set(const QString &key, const QVariant &value) final
constexpr char DbName[]