summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorten Sørvig <morten.sorvig@qt.io>2022-09-12 13:25:18 +0200
committerMorten Sørvig <morten.sorvig@qt.io>2022-09-12 19:50:02 +0200
commit1f6213713d8e51b8d236c6be41c92b36fe95e3c1 (patch)
treef463218a615de1317576a3fc68eed8806f5c31f5
parentffa3c3579acaf3291b0cfa7e5670313d576d5d49 (diff)
wasm: add Window.localStorage settings backend
Window.localStorage provides a synchronous API for storing saving data across browsing sessions, and is a good match for QSettings. Storage is limited to 5MB per origin, which should be sufficient for typical application settings. Window.localStorage is shared by all pages/apps on the origin (e.g. "https://qt.io" is one origin), which makes key collisions possible. To avoid this the key structure is "qt-v0-org-app-userkey", where both the organization and application name is used to differentiate between keys, and "v0" is a version tag to allow changing the key structure in the future. We reserve the "qt" prefix for keys written by QSettings. Add the new implementation as QWasmLocalStorageSettingsPrivate, rename the existing settings backend to QWasmIDBSettingsPrivate. Make QSettingsPrivate::create() support backend selection using the QSettings::Format enum. It now also supports selecting the Ini backend, which can be used to store larger amounts of settings, for example to a file on IDBFS. (alternatively IDBSettings + asyncify could also be used for this case) Change-Id: If70aa488635018218bc2a19803c4a719732c0004 Reviewed-by: David Skoland <david.skoland@qt.io>
-rw-r--r--src/corelib/io/qsettings.h6
-rw-r--r--src/corelib/io/qsettings_p.h6
-rw-r--r--src/corelib/io/qsettings_wasm.cpp286
3 files changed, 227 insertions, 71 deletions
diff --git a/src/corelib/io/qsettings.h b/src/corelib/io/qsettings.h
index b8fba349ca..5d2c330728 100644
--- a/src/corelib/io/qsettings.h
+++ b/src/corelib/io/qsettings.h
@@ -54,6 +54,12 @@ public:
Registry64Format,
#endif
+#if defined(Q_OS_WASM)
+ // FIXME: add public API in next minor release.
+ // WebLocalStorageFormat (IniFormat + 1)
+ // WebIDBSFormat (IniFormat + 2)
+#endif
+
InvalidFormat = 16,
CustomFormat1,
CustomFormat2,
diff --git a/src/corelib/io/qsettings_p.h b/src/corelib/io/qsettings_p.h
index d1ea37ea0c..2429820242 100644
--- a/src/corelib/io/qsettings_p.h
+++ b/src/corelib/io/qsettings_p.h
@@ -216,10 +216,6 @@ protected:
mutable QSettings::Status status;
};
-#ifdef Q_OS_WASM
-class QWasmSettingsPrivate;
-#endif
-
class QConfFileSettingsPrivate : public QSettingsPrivate
{
public:
@@ -266,7 +262,7 @@ private:
Qt::CaseSensitivity caseSensitivity;
qsizetype nextPosition;
#ifdef Q_OS_WASM
- friend class QWasmSettingsPrivate;
+ friend class QWasmIDBSettingsPrivate;
#endif
};
diff --git a/src/corelib/io/qsettings_wasm.cpp b/src/corelib/io/qsettings_wasm.cpp
index 5c15e6d89b..8404a526b6 100644
--- a/src/corelib/io/qsettings_wasm.cpp
+++ b/src/corelib/io/qsettings_wasm.cpp
@@ -1,4 +1,4 @@
-// Copyright (C) 2019 The Qt Company Ltd.
+// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qsettings.h"
@@ -16,20 +16,160 @@
#include <QList>
#include <emscripten.h>
+#include <emscripten/val.h>
QT_BEGIN_NAMESPACE
+using emscripten::val;
using namespace Qt::StringLiterals;
-static bool isReadReady = false;
+//
+// Native settings implementation for WebAssembly using window.localStorage
+// as the storage backend. localStorage is a key-value store with a synchronous
+// API and a 5MB storage limit.
+//
+class QWasmLocalStorageSettingsPrivate final : public QSettingsPrivate
+{
+public:
+ QWasmLocalStorageSettingsPrivate(QSettings::Scope scope, const QString &organization,
+ const QString &application);
+
+ void remove(const QString &key) final;
+ void set(const QString &key, const QVariant &value) final;
+ std::optional<QVariant> get(const QString &key) const final;
+ QStringList children(const QString &prefix, ChildSpec spec) const final;
+ void clear() final;
+ void sync() final;
+ void flush() final;
+ bool isWritable() const final;
+ QString fileName() const final;
+
+private:
+ QString prependStoragePrefix(const QString &key) const;
+ QStringView removeStoragePrefix(QStringView key) const;
+ val m_localStorage = val::global("window")["localStorage"];
+ QString m_keyPrefix;
+};
+
+QWasmLocalStorageSettingsPrivate::QWasmLocalStorageSettingsPrivate(QSettings::Scope scope,
+ const QString &organization,
+ const QString &application)
+ : QSettingsPrivate(QSettings::NativeFormat, scope, organization, application)
+{
+ // The key prefix contians "qt" to separate Qt keys from other keys on localStorage, a
+ // version tag to allow for making changes to the key format in the future, the org
+ // and app names.
+ //
+ // User code could could create separate settings object with different org and app names,
+ // and would expect them to have separate settings. Also, different webassembly instanaces
+ // on the page could write to the same window.localStorage. Add the org and app name
+ // to the key prefix to differentiate, even if that leads to keys with redundant sectons
+ // for the common case of a single org and app name.
+ const QLatin1String separator("-");
+ const QLatin1String doubleSeparator("--");
+ const QString escapedOrganization = QString(organization).replace(separator, doubleSeparator);
+ const QString escapedApplication = QString(application).replace(separator, doubleSeparator);
+ const QLatin1String prefix("qt-v0-");
+ m_keyPrefix.reserve(prefix.length() + escapedOrganization.length() +
+ escapedApplication.length() + separator.length() * 2);
+ m_keyPrefix = prefix + escapedOrganization + separator + escapedApplication + separator;
+}
+
+void QWasmLocalStorageSettingsPrivate::remove(const QString &key)
+{
+ const std::string keyString = prependStoragePrefix(key).toStdString();
+ m_localStorage.call<val>("removeItem", keyString);
+}
+
+void QWasmLocalStorageSettingsPrivate::set(const QString &key, const QVariant &value)
+{
+ const std::string keyString = prependStoragePrefix(key).toStdString();
+ const std::string valueString = QSettingsPrivate::variantToString(value).toStdString();
+ m_localStorage.call<void>("setItem", keyString, valueString);
+}
+
+std::optional<QVariant> QWasmLocalStorageSettingsPrivate::get(const QString &key) const
+{
+ const std::string keyString = prependStoragePrefix(key).toStdString();
+ const emscripten::val value = m_localStorage.call<val>("getItem", keyString);
+ if (value.isNull())
+ return std::nullopt;
+ const QString valueString = QString::fromStdString(value.as<std::string>());
+ return QSettingsPrivate::stringToVariant(valueString);
+}
+
+QStringList QWasmLocalStorageSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
+{
+ // Loop through all keys on window.localStorage, return Qt keys belonging to
+ // this application, with the correct prefix, and according to ChildSpec.
+ QStringList children;
+ const int length = m_localStorage["length"].as<int>();
+ for (int i = 0; i < length; ++i) {
+ const QString keyString =
+ QString::fromStdString(m_localStorage.call<val>("key", i).as<std::string>());
+
+ const QStringView key = removeStoragePrefix(QStringView(keyString));
+ if (key.isEmpty())
+ continue;
+ if (!key.startsWith(prefix))
+ continue;
+
+ QSettingsPrivate::processChild(key.sliced(prefix.length()), spec, children);
+ }
+
+ return children;
+}
+
+void QWasmLocalStorageSettingsPrivate::clear()
+{
+ // Remove all Qt keys from window.localStorage
+ const int length = m_localStorage["length"].as<int>();
+ for (int i = 0; i < length; ++i) {
+ std::string fullKey = (m_localStorage.call<val>("key", i).as<std::string>());
+ QString key = QString::fromStdString(fullKey);
+ if (removeStoragePrefix(QStringView(key)).isEmpty() == false)
+ m_localStorage.call<val>("removeItem", fullKey);
+ }
+}
+
+void QWasmLocalStorageSettingsPrivate::sync() { }
+
+void QWasmLocalStorageSettingsPrivate::flush() { }
-class QWasmSettingsPrivate : public QConfFileSettingsPrivate
+bool QWasmLocalStorageSettingsPrivate::isWritable() const
+{
+ return true;
+}
+
+QString QWasmLocalStorageSettingsPrivate::fileName() const
+{
+ return QString();
+}
+
+QString QWasmLocalStorageSettingsPrivate::prependStoragePrefix(const QString &key) const
+{
+ return m_keyPrefix + key;
+}
+
+QStringView QWasmLocalStorageSettingsPrivate::removeStoragePrefix(QStringView key) const
+{
+ // Return the key slice after m_keyPrefix, or an empty string view if no match
+ if (!key.startsWith(m_keyPrefix))
+ return QStringView();
+ return key.sliced(m_keyPrefix.length());
+}
+
+//
+// Native settings implementation for WebAssembly using the indexed database as
+// the storage backend
+//
+class QWasmIDBSettingsPrivate : public QConfFileSettingsPrivate
{
public:
- QWasmSettingsPrivate(QSettings::Scope scope, const QString &organization,
- const QString &application);
- ~QWasmSettingsPrivate();
- static QWasmSettingsPrivate *get(void *userData);
+ QWasmIDBSettingsPrivate(QSettings::Scope scope, const QString &organization,
+ const QString &application);
+ ~QWasmIDBSettingsPrivate();
+ static QWasmIDBSettingsPrivate *get(void *userData);
std::optional<QVariant> get(const QString &key) const override;
QStringList children(const QString &prefix, ChildSpec spec) const override;
@@ -46,14 +186,15 @@ public:
private:
QString databaseName;
QString id;
- static QList<QWasmSettingsPrivate *> liveSettings;
+ static QList<QWasmIDBSettingsPrivate *> liveSettings;
};
-QList<QWasmSettingsPrivate *> QWasmSettingsPrivate::liveSettings;
+QList<QWasmIDBSettingsPrivate *> QWasmIDBSettingsPrivate::liveSettings;
+static bool isReadReady = false;
-static void QWasmSettingsPrivate_onLoad(void *userData, void *dataPtr, int size)
+static void QWasmIDBSettingsPrivate_onLoad(void *userData, void *dataPtr, int size)
{
- QWasmSettingsPrivate *settings = QWasmSettingsPrivate::get(userData);
+ QWasmIDBSettingsPrivate *settings = QWasmIDBSettingsPrivate::get(userData);
if (!settings)
return;
@@ -70,21 +211,21 @@ static void QWasmSettingsPrivate_onLoad(void *userData, void *dataPtr, int size)
}
}
-static void QWasmSettingsPrivate_onError(void *userData)
+static void QWasmIDBSettingsPrivate_onError(void *userData)
{
- if (QWasmSettingsPrivate *settings = QWasmSettingsPrivate::get(userData))
+ if (QWasmIDBSettingsPrivate *settings = QWasmIDBSettingsPrivate::get(userData))
settings->setStatus(QSettings::AccessError);
}
-static void QWasmSettingsPrivate_onStore(void *userData)
+static void QWasmIDBSettingsPrivate_onStore(void *userData)
{
- if (QWasmSettingsPrivate *settings = QWasmSettingsPrivate::get(userData))
+ if (QWasmIDBSettingsPrivate *settings = QWasmIDBSettingsPrivate::get(userData))
settings->setStatus(QSettings::NoError);
}
-static void QWasmSettingsPrivate_onCheck(void *userData, int exists)
+static void QWasmIDBSettingsPrivate_onCheck(void *userData, int exists)
{
- if (QWasmSettingsPrivate *settings = QWasmSettingsPrivate::get(userData)) {
+ if (QWasmIDBSettingsPrivate *settings = QWasmIDBSettingsPrivate::get(userData)) {
if (exists)
settings->loadLocal(settings->fileName().toLocal8Bit());
else
@@ -92,31 +233,9 @@ static void QWasmSettingsPrivate_onCheck(void *userData, int exists)
}
}
-QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format,
- QSettings::Scope scope,
- const QString &organization,
- const QString &application)
-{
- Q_UNUSED(format);
- if (organization == "Qt"_L1)
- {
- QString organizationDomain = QCoreApplication::organizationDomain();
- QString applicationName = QCoreApplication::applicationName();
-
- QSettingsPrivate *newSettings;
- newSettings = new QWasmSettingsPrivate(scope, organizationDomain, applicationName);
-
- newSettings->beginGroupOrArray(QSettingsGroup(normalizedKey(organization)));
- if (!application.isEmpty())
- newSettings->beginGroupOrArray(QSettingsGroup(normalizedKey(application)));
-
- return newSettings;
- }
- return new QWasmSettingsPrivate(scope, organization, application);
-}
-
-QWasmSettingsPrivate::QWasmSettingsPrivate(QSettings::Scope scope, const QString &organization,
- const QString &application)
+QWasmIDBSettingsPrivate::QWasmIDBSettingsPrivate(QSettings::Scope scope,
+ const QString &organization,
+ const QString &application)
: QConfFileSettingsPrivate(QSettings::NativeFormat, scope, organization, application)
{
liveSettings.push_back(this);
@@ -128,29 +247,29 @@ QWasmSettingsPrivate::QWasmSettingsPrivate(QSettings::Scope scope, const QString
emscripten_idb_async_exists("/home/web_user",
fileName().toLocal8Bit(),
reinterpret_cast<void*>(this),
- QWasmSettingsPrivate_onCheck,
- QWasmSettingsPrivate_onError);
+ QWasmIDBSettingsPrivate_onCheck,
+ QWasmIDBSettingsPrivate_onError);
}
-QWasmSettingsPrivate::~QWasmSettingsPrivate()
+QWasmIDBSettingsPrivate::~QWasmIDBSettingsPrivate()
{
liveSettings.removeAll(this);
}
-QWasmSettingsPrivate *QWasmSettingsPrivate::get(void *userData)
+QWasmIDBSettingsPrivate *QWasmIDBSettingsPrivate::get(void *userData)
{
- if (QWasmSettingsPrivate::liveSettings.contains(userData))
- return reinterpret_cast<QWasmSettingsPrivate *>(userData);
+ if (QWasmIDBSettingsPrivate::liveSettings.contains(userData))
+ return reinterpret_cast<QWasmIDBSettingsPrivate *>(userData);
return nullptr;
}
- void QWasmSettingsPrivate::initAccess()
+void QWasmIDBSettingsPrivate::initAccess()
{
if (isReadReady)
QConfFileSettingsPrivate::initAccess();
}
-std::optional<QVariant> QWasmSettingsPrivate::get(const QString &key) const
+std::optional<QVariant> QWasmIDBSettingsPrivate::get(const QString &key) const
{
if (isReadReady)
return QConfFileSettingsPrivate::get(key);
@@ -158,22 +277,22 @@ std::optional<QVariant> QWasmSettingsPrivate::get(const QString &key) const
return std::nullopt;
}
-QStringList QWasmSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
+QStringList QWasmIDBSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
{
return QConfFileSettingsPrivate::children(prefix, spec);
}
-void QWasmSettingsPrivate::clear()
+void QWasmIDBSettingsPrivate::clear()
{
QConfFileSettingsPrivate::clear();
emscripten_idb_async_delete("/home/web_user",
fileName().toLocal8Bit(),
reinterpret_cast<void*>(this),
- QWasmSettingsPrivate_onStore,
- QWasmSettingsPrivate_onError);
+ QWasmIDBSettingsPrivate_onStore,
+ QWasmIDBSettingsPrivate_onError);
}
-void QWasmSettingsPrivate::sync()
+void QWasmIDBSettingsPrivate::sync()
{
QConfFileSettingsPrivate::sync();
@@ -186,22 +305,22 @@ void QWasmSettingsPrivate::sync()
reinterpret_cast<void *>(dataPointer.data()),
dataPointer.length(),
reinterpret_cast<void*>(this),
- QWasmSettingsPrivate_onStore,
- QWasmSettingsPrivate_onError);
+ QWasmIDBSettingsPrivate_onStore,
+ QWasmIDBSettingsPrivate_onError);
}
}
-void QWasmSettingsPrivate::flush()
+void QWasmIDBSettingsPrivate::flush()
{
sync();
}
-bool QWasmSettingsPrivate::isWritable() const
+bool QWasmIDBSettingsPrivate::isWritable() const
{
return isReadReady && QConfFileSettingsPrivate::isWritable();
}
-void QWasmSettingsPrivate::syncToLocal(const char *data, int size)
+void QWasmIDBSettingsPrivate::syncToLocal(const char *data, int size)
{
QFile file(fileName());
@@ -214,27 +333,62 @@ void QWasmSettingsPrivate::syncToLocal(const char *data, int size)
reinterpret_cast<void *>(data.data()),
data.length(),
reinterpret_cast<void*>(this),
- QWasmSettingsPrivate_onStore,
- QWasmSettingsPrivate_onError);
+ QWasmIDBSettingsPrivate_onStore,
+ QWasmIDBSettingsPrivate_onError);
setReady();
}
}
-void QWasmSettingsPrivate::loadLocal(const QByteArray &filename)
+void QWasmIDBSettingsPrivate::loadLocal(const QByteArray &filename)
{
emscripten_idb_async_load("/home/web_user",
filename.data(),
reinterpret_cast<void*>(this),
- QWasmSettingsPrivate_onLoad,
- QWasmSettingsPrivate_onError);
+ QWasmIDBSettingsPrivate_onLoad,
+ QWasmIDBSettingsPrivate_onError);
}
-void QWasmSettingsPrivate::setReady()
+void QWasmIDBSettingsPrivate::setReady()
{
isReadReady = true;
setStatus(QSettings::NoError);
QConfFileSettingsPrivate::initAccess();
}
+QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format, QSettings::Scope scope,
+ const QString &organization, const QString &application)
+{
+ const auto WebLocalStorageFormat = QSettings::IniFormat + 1;
+ const auto WebIdbFormat = QSettings::IniFormat + 2;
+
+ // Make WebLocalStorageFormat the default native format
+ if (format == QSettings::NativeFormat)
+ format = QSettings::Format(WebLocalStorageFormat);
+
+ // Check if cookies are enabled (required for using persistent storage)
+ const bool cookiesEnabled = val::global("navigator")["cookieEnabled"].as<bool>();
+ constexpr QLatin1StringView cookiesWarningMessage
+ ("QSettings::%1 requires cookies, falling back to IniFormat with temporary file");
+ if (format == WebLocalStorageFormat && !cookiesEnabled) {
+ qWarning() << cookiesWarningMessage.arg("WebLocalStorageFormat");
+ format = QSettings::IniFormat;
+ } else if (format == WebIdbFormat && !cookiesEnabled) {
+ qWarning() << cookiesWarningMessage.arg("WebIdbFormat");
+ format = QSettings::IniFormat;
+ }
+
+ // Create settings backend according to selected format
+ if (format == WebLocalStorageFormat) {
+ return new QWasmLocalStorageSettingsPrivate(scope, organization, application);
+ } else if (format == WebIdbFormat) {
+ return new QWasmIDBSettingsPrivate(scope, organization, application);
+ } else if (format == QSettings::IniFormat) {
+ return new QConfFileSettingsPrivate(format, scope, organization, application);
+ }
+
+ qWarning() << "Unsupported settings format" << format;
+ return nullptr;
+}
+
QT_END_NAMESPACE
#endif // QT_NO_SETTINGS