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