diff options
-rw-r--r-- | src/corelib/io/qsettings.h | 6 | ||||
-rw-r--r-- | src/corelib/io/qsettings_p.h | 6 | ||||
-rw-r--r-- | src/corelib/io/qsettings_wasm.cpp | 286 |
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 |