diff options
Diffstat (limited to 'src/corelib/io/qsettings_wasm.cpp')
-rw-r--r-- | src/corelib/io/qsettings_wasm.cpp | 498 |
1 files changed, 321 insertions, 177 deletions
diff --git a/src/corelib/io/qsettings_wasm.cpp b/src/corelib/io/qsettings_wasm.cpp index 9e016aa4f4..7d80ff82d3 100644 --- a/src/corelib/io/qsettings_wasm.cpp +++ b/src/corelib/io/qsettings_wasm.cpp @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// 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" #ifndef QT_NO_SETTINGS @@ -46,213 +10,393 @@ #include <QFile> #endif // QT_NO_QOBJECT #include <QDebug> +#include <QtCore/private/qstdweb_p.h> #include <QFileInfo> #include <QDir> +#include <QList> +#include <QSet> + #include <emscripten.h> +# include <emscripten/proxying.h> +# include <emscripten/threading.h> +# include <emscripten/val.h> QT_BEGIN_NAMESPACE -static bool isReadReady = false; +using emscripten::val; +using namespace Qt::StringLiterals; -class QWasmSettingsPrivate : public QConfFileSettingsPrivate +namespace { +QStringView keyNameFromPrefixedStorageName(QStringView prefix, QStringView prefixedStorageName) +{ + // Return the key slice after m_keyPrefix, or an empty string view if no match + if (!prefixedStorageName.startsWith(prefix)) + return QStringView(); + return prefixedStorageName.sliced(prefix.length()); +} +} // namespace + +// +// 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: - QWasmSettingsPrivate(QSettings::Scope scope, const QString &organization, - const QString &application); - ~QWasmSettingsPrivate(); - - std::optional<QVariant> get(const QString &key) const override; - QStringList children(const QString &prefix, ChildSpec spec) const override; - void clear() override; - void sync() override; - void flush() override; - bool isWritable() const override; - - void syncToLocal(const char *data, int size); - void loadLocal(const QByteArray &filename); - void setReady(); - void initAccess() override; + QWasmLocalStorageSettingsPrivate(QSettings::Scope scope, const QString &organization, + const QString &application); + ~QWasmLocalStorageSettingsPrivate() final = default; + + 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 databaseName; - QString id; + QStringList m_keyPrefixes; }; -static void QWasmSettingsPrivate_onLoad(void *userData, void *dataPtr, int size) +QWasmLocalStorageSettingsPrivate::QWasmLocalStorageSettingsPrivate(QSettings::Scope scope, + const QString &organization, + const QString &application) + : QSettingsPrivate(QSettings::NativeFormat, scope, organization, application) { - QWasmSettingsPrivate *wasm = reinterpret_cast<QWasmSettingsPrivate *>(userData); - - QFile file(wasm->fileName()); - QFileInfo fileInfo(wasm->fileName()); - QDir dir(fileInfo.path()); - if (!dir.exists()) - dir.mkpath(fileInfo.path()); + if (organization.isEmpty()) { + setStatus(QSettings::AccessError); + return; + } - if (file.open(QFile::WriteOnly)) { - file.write(reinterpret_cast<char *>(dataPtr), size); - file.close(); - wasm->setReady(); + // 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 instances + // 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 sections + // for the common case of a single org and app name. + // + // Also, the common Qt mechanism for user/system scope and all-application settings are + // implemented, using different prefixes. + const QString allAppsSetting = QStringLiteral("all-apps"); + const QString systemSetting = QStringLiteral("sys-tem"); + + const QLatin1String separator("-"); + const QLatin1String doubleSeparator("--"); + const QString escapedOrganization = QString(organization).replace(separator, doubleSeparator); + const QString escapedApplication = QString(application).replace(separator, doubleSeparator); + const QString prefix = "qt-v0-" + escapedOrganization + separator; + if (scope == QSettings::Scope::UserScope) { + if (!escapedApplication.isEmpty()) + m_keyPrefixes.push_back(prefix + escapedApplication + separator); + m_keyPrefixes.push_back(prefix + allAppsSetting + separator); + } + if (!escapedApplication.isEmpty()) { + m_keyPrefixes.push_back(prefix + escapedApplication + separator + systemSetting + + separator); } + m_keyPrefixes.push_back(prefix + allAppsSetting + separator + systemSetting + separator); } -static void QWasmSettingsPrivate_onError(void *userData) +void QWasmLocalStorageSettingsPrivate::remove(const QString &key) { - QWasmSettingsPrivate *wasm = reinterpret_cast<QWasmSettingsPrivate *>(userData); - if (wasm) - wasm->setStatus(QSettings::AccessError); + const std::string removed = QString(m_keyPrefixes.first() + key).toStdString(); + + qstdweb::runTaskOnMainThread<void>([this, &removed, &key]() { + std::vector<std::string> children = { removed }; + const int length = val::global("window")["localStorage"]["length"].as<int>(); + for (int i = 0; i < length; ++i) { + const QString storedKeyWithPrefix = QString::fromStdString( + val::global("window")["localStorage"].call<val>("key", i).as<std::string>()); + + const QStringView storedKey = keyNameFromPrefixedStorageName( + m_keyPrefixes.first(), QStringView(storedKeyWithPrefix)); + if (storedKey.isEmpty() || !storedKey.startsWith(key)) + continue; + + children.push_back(storedKeyWithPrefix.toStdString()); + } + + for (const auto &child : children) + val::global("window")["localStorage"].call<val>("removeItem", child); + }); } -static void QWasmSettingsPrivate_onStore(void *userData) +void QWasmLocalStorageSettingsPrivate::set(const QString &key, const QVariant &value) { - QWasmSettingsPrivate *wasm = reinterpret_cast<QWasmSettingsPrivate *>(userData); - if (wasm) - wasm->setStatus(QSettings::NoError); + qstdweb::runTaskOnMainThread<void>([this, &key, &value]() { + const std::string keyString = QString(m_keyPrefixes.first() + key).toStdString(); + const std::string valueString = QSettingsPrivate::variantToString(value).toStdString(); + val::global("window")["localStorage"].call<void>("setItem", keyString, valueString); + }); } -static void QWasmSettingsPrivate_onCheck(void *userData, int exists) +std::optional<QVariant> QWasmLocalStorageSettingsPrivate::get(const QString &key) const { - QWasmSettingsPrivate *wasm = reinterpret_cast<QWasmSettingsPrivate *>(userData); - if (wasm) { - if (exists) - wasm->loadLocal(wasm->fileName().toLocal8Bit()); - else - wasm->setReady(); - } + return qstdweb::runTaskOnMainThread<std::optional<QVariant>>( + [this, &key]() -> std::optional<QVariant> { + for (const auto &prefix : m_keyPrefixes) { + const std::string keyString = QString(prefix + key).toStdString(); + const emscripten::val value = + val::global("window")["localStorage"].call<val>("getItem", keyString); + if (!value.isNull()) { + return QSettingsPrivate::stringToVariant( + QString::fromStdString(value.as<std::string>())); + } + if (!fallbacks) { + return std::nullopt; + } + } + return std::nullopt; + }); } -QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format, - QSettings::Scope scope, - const QString &organization, - const QString &application) +QStringList QWasmLocalStorageSettingsPrivate::children(const QString &prefix, ChildSpec spec) const { - Q_UNUSED(format); - if (organization == QLatin1String("Qt")) - { - QString organizationDomain = QCoreApplication::organizationDomain(); - QString applicationName = QCoreApplication::applicationName(); + return qstdweb::runTaskOnMainThread<QStringList>([this, &prefix, &spec]() -> QStringList { + QSet<QString> nodes; + // 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 = val::global("window")["localStorage"]["length"].as<int>(); + for (int i = 0; i < length; ++i) { + for (const auto &storagePrefix : m_keyPrefixes) { + const QString keyString = + QString::fromStdString(val::global("window")["localStorage"] + .call<val>("key", i) + .as<std::string>()); + + const QStringView key = + keyNameFromPrefixedStorageName(storagePrefix, QStringView(keyString)); + if (!key.isEmpty() && key.startsWith(prefix)) { + QStringList children; + QSettingsPrivate::processChild(key.sliced(prefix.length()), spec, children); + if (!children.isEmpty()) + nodes.insert(children.first()); + } + if (!fallbacks) + break; + } + } + + return QStringList(nodes.begin(), nodes.end()); + }); +} - QSettingsPrivate *newSettings; - newSettings = new QWasmSettingsPrivate(scope, organizationDomain, applicationName); +void QWasmLocalStorageSettingsPrivate::clear() +{ + qstdweb::runTaskOnMainThread<void>([this]() { + // Get all Qt keys from window.localStorage + const int length = val::global("window")["localStorage"]["length"].as<int>(); + QStringList keys; + keys.reserve(length); + for (int i = 0; i < length; ++i) + keys.append(QString::fromStdString( + (val::global("window")["localStorage"].call<val>("key", i).as<std::string>()))); + + // Remove all Qt keys. Note that localStorage does not guarantee a stable + // iteration order when the storage is mutated, which is why removal is done + // in a second step after getting all keys. + for (const QString &key : keys) { + if (!keyNameFromPrefixedStorageName(m_keyPrefixes.first(), key).isEmpty()) + val::global("window")["localStorage"].call<val>("removeItem", key.toStdString()); + } + }); +} - newSettings->beginGroupOrArray(QSettingsGroup(normalizedKey(organization))); - if (!application.isEmpty()) - newSettings->beginGroupOrArray(QSettingsGroup(normalizedKey(application))); +void QWasmLocalStorageSettingsPrivate::sync() { } - return newSettings; - } - return new QWasmSettingsPrivate(scope, organization, application); -} +void QWasmLocalStorageSettingsPrivate::flush() { } -QWasmSettingsPrivate::QWasmSettingsPrivate(QSettings::Scope scope, const QString &organization, - const QString &application) - : QConfFileSettingsPrivate(QSettings::NativeFormat, scope, organization, application) +bool QWasmLocalStorageSettingsPrivate::isWritable() const { - setStatus(QSettings::AccessError); // access error until sandbox gets loaded - databaseName = organization; - id = application; - - emscripten_idb_async_exists("/home/web_user", - fileName().toLocal8Bit(), - reinterpret_cast<void*>(this), - QWasmSettingsPrivate_onCheck, - QWasmSettingsPrivate_onError); + return true; } -QWasmSettingsPrivate::~QWasmSettingsPrivate() +QString QWasmLocalStorageSettingsPrivate::fileName() const { + return QString(); } - void QWasmSettingsPrivate::initAccess() +// +// Native settings implementation for WebAssembly using the indexed database as +// the storage backend +// +class QWasmIDBSettingsPrivate : public QConfFileSettingsPrivate { - if (isReadReady) - QConfFileSettingsPrivate::initAccess(); -} +public: + QWasmIDBSettingsPrivate(QSettings::Scope scope, const QString &organization, + const QString &application); + ~QWasmIDBSettingsPrivate(); + + void clear() override; + void sync() override; + +private: + bool writeSettingsToTemporaryFile(const QString &fileName, void *dataPtr, int size); + void loadIndexedDBFiles(); + + + QString databaseName; + QString id; +}; -std::optional<QVariant> QWasmSettingsPrivate::get(const QString &key) const +constexpr char DbName[] = "/home/web_user"; + +QWasmIDBSettingsPrivate::QWasmIDBSettingsPrivate(QSettings::Scope scope, + const QString &organization, + const QString &application) + : QConfFileSettingsPrivate(QSettings::WebIndexedDBFormat, scope, organization, application) { - if (isReadReady) - return QConfFileSettingsPrivate::get(key); + Q_ASSERT_X(qstdweb::haveJspi(), Q_FUNC_INFO, "QWasmIDBSettingsPrivate needs JSPI to work"); + + if (organization.isEmpty()) { + setStatus(QSettings::AccessError); + return; + } + + databaseName = organization; + id = application; - return std::nullopt; + loadIndexedDBFiles(); + + QConfFileSettingsPrivate::initAccess(); } -QStringList QWasmSettingsPrivate::children(const QString &prefix, ChildSpec spec) const +QWasmIDBSettingsPrivate::~QWasmIDBSettingsPrivate() = default; + +bool QWasmIDBSettingsPrivate::writeSettingsToTemporaryFile(const QString &fileName, void *dataPtr, + int size) { - return QConfFileSettingsPrivate::children(prefix, spec); + QFile file(fileName); + QFileInfo fileInfo(fileName); + QDir dir(fileInfo.path()); + if (!dir.exists()) + dir.mkpath(fileInfo.path()); + + if (!file.open(QFile::WriteOnly)) + return false; + + return size == file.write(reinterpret_cast<char *>(dataPtr), size); } -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); + + int error = 0; + emscripten_idb_delete(DbName, fileName().toLocal8Bit(), &error); + setStatus(!!error ? QSettings::AccessError : QSettings::NoError); } -void QWasmSettingsPrivate::sync() +void QWasmIDBSettingsPrivate::sync() { + // Reload the files, in case there were any changes in IndexedDB, and flush them to disk. + // Thanks to this, QConfFileSettingsPrivate::sync will handle key merging correctly. + loadIndexedDBFiles(); + QConfFileSettingsPrivate::sync(); QFile file(fileName()); if (file.open(QFile::ReadOnly)) { QByteArray dataPointer = file.readAll(); - emscripten_idb_async_store("/home/web_user", - fileName().toLocal8Bit(), - reinterpret_cast<void *>(dataPointer.data()), - dataPointer.length(), - reinterpret_cast<void*>(this), - QWasmSettingsPrivate_onStore, - QWasmSettingsPrivate_onError); + int error = 0; + emscripten_idb_store(DbName, fileName().toLocal8Bit(), + reinterpret_cast<void *>(dataPointer.data()), dataPointer.length(), + &error); + setStatus(!!error ? QSettings::AccessError : QSettings::NoError); } } -void QWasmSettingsPrivate::flush() -{ - sync(); -} - -bool QWasmSettingsPrivate::isWritable() const -{ - return isReadReady && QConfFileSettingsPrivate::isWritable(); -} - -void QWasmSettingsPrivate::syncToLocal(const char *data, int size) +void QWasmIDBSettingsPrivate::loadIndexedDBFiles() { - QFile file(fileName()); - - if (file.open(QFile::WriteOnly)) { - file.write(data, size + 1); - QByteArray data = file.readAll(); - - emscripten_idb_async_store("/home/web_user", - fileName().toLocal8Bit(), - reinterpret_cast<void *>(data.data()), - data.length(), - reinterpret_cast<void*>(this), - QWasmSettingsPrivate_onStore, - QWasmSettingsPrivate_onError); - setReady(); + for (const auto *confFile : getConfFiles()) { + int exists = 0; + int error = 0; + emscripten_idb_exists(DbName, confFile->name.toLocal8Bit(), &exists, &error); + if (error) { + setStatus(QSettings::AccessError); + return; + } + if (exists) { + void *contents; + int size; + emscripten_idb_load(DbName, confFile->name.toLocal8Bit(), &contents, &size, &error); + if (error || !writeSettingsToTemporaryFile(confFile->name, contents, size)) { + setStatus(QSettings::AccessError); + return; + } + } } } -void QWasmSettingsPrivate::loadLocal(const QByteArray &filename) +QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format, QSettings::Scope scope, + const QString &organization, const QString &application) { - emscripten_idb_async_load("/home/web_user", - filename.data(), - reinterpret_cast<void*>(this), - QWasmSettingsPrivate_onLoad, - QWasmSettingsPrivate_onError); -} + // Make WebLocalStorageFormat the default native format + if (format == QSettings::NativeFormat) + format = QSettings::WebLocalStorageFormat; + + // Check if cookies are enabled (required for using persistent storage) + + const bool cookiesEnabled = qstdweb::runTaskOnMainThread<bool>( + []() { return val::global("navigator")["cookieEnabled"].as<bool>(); }); + + constexpr QLatin1StringView cookiesWarningMessage( + "QSettings::%1 requires cookies, falling back to IniFormat with temporary file"); + if (!cookiesEnabled) { + if (format == QSettings::WebLocalStorageFormat) { + qWarning() << cookiesWarningMessage.arg("WebLocalStorageFormat"); + format = QSettings::IniFormat; + } else if (format == QSettings::WebIndexedDBFormat) { + qWarning() << cookiesWarningMessage.arg("WebIndexedDBFormat"); + format = QSettings::IniFormat; + } + } + if (format == QSettings::WebIndexedDBFormat && !qstdweb::haveJspi()) { + qWarning() << "QSettings::WebIndexedDBFormat requires JSPI, falling back to IniFormat with " + "temporary file"; + format = QSettings::IniFormat; + } -void QWasmSettingsPrivate::setReady() -{ - isReadReady = true; - setStatus(QSettings::NoError); - QConfFileSettingsPrivate::initAccess(); + // Create settings backend according to selected format + switch (format) { + case QSettings::Format::WebLocalStorageFormat: + return new QWasmLocalStorageSettingsPrivate(scope, organization, application); + case QSettings::Format::WebIndexedDBFormat: + return new QWasmIDBSettingsPrivate(scope, organization, application); + case QSettings::Format::IniFormat: + case QSettings::Format::CustomFormat1: + case QSettings::Format::CustomFormat2: + case QSettings::Format::CustomFormat3: + case QSettings::Format::CustomFormat4: + case QSettings::Format::CustomFormat5: + case QSettings::Format::CustomFormat6: + case QSettings::Format::CustomFormat7: + case QSettings::Format::CustomFormat8: + case QSettings::Format::CustomFormat9: + case QSettings::Format::CustomFormat10: + case QSettings::Format::CustomFormat11: + case QSettings::Format::CustomFormat12: + case QSettings::Format::CustomFormat13: + case QSettings::Format::CustomFormat14: + case QSettings::Format::CustomFormat15: + case QSettings::Format::CustomFormat16: + return new QConfFileSettingsPrivate(format, scope, organization, application); + case QSettings::Format::InvalidFormat: + return nullptr; + case QSettings::Format::NativeFormat: + Q_UNREACHABLE(); + break; + } } QT_END_NAMESPACE |