diff --git a/src/corelib/io/qsettings_wasm.cpp b/src/corelib/io/qsettings_wasm.cpp index 15ab688abe5..5043f2f8588 100644 --- a/src/corelib/io/qsettings_wasm.cpp +++ b/src/corelib/io/qsettings_wasm.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,16 @@ QT_BEGIN_NAMESPACE using emscripten::val; using namespace Qt::StringLiterals; +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 @@ -33,6 +44,7 @@ class QWasmLocalStorageSettingsPrivate final : public QSettingsPrivate public: 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; @@ -45,10 +57,8 @@ public: 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; + QStringList m_keyPrefixes; }; QWasmLocalStorageSettingsPrivate::QWasmLocalStorageSettingsPrivate(QSettings::Scope scope, @@ -56,87 +66,129 @@ QWasmLocalStorageSettingsPrivate::QWasmLocalStorageSettingsPrivate(QSettings::Sc const QString &application) : QSettingsPrivate(QSettings::NativeFormat, scope, organization, application) { + if (organization.isEmpty()) { + setStatus(QSettings::AccessError); + return; + } + // 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 + // 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 sectons + // 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 QLatin1String prefix("qt-v0-"); - m_keyPrefix.reserve(prefix.length() + escapedOrganization.length() + - escapedApplication.length() + separator.length() * 2); - m_keyPrefix = prefix + escapedOrganization + separator + escapedApplication + separator; + 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); } void QWasmLocalStorageSettingsPrivate::remove(const QString &key) { - const std::string keyString = prependStoragePrefix(key).toStdString(); - m_localStorage.call("removeItem", keyString); + const std::string removed = QString(m_keyPrefixes.first() + key).toStdString(); + + std::vector children = { removed }; + const int length = m_localStorage["length"].as(); + for (int i = 0; i < length; ++i) { + const QString storedKeyWithPrefix = + QString::fromStdString(m_localStorage.call("key", i).as()); + + 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) + m_localStorage.call("removeItem", child); } void QWasmLocalStorageSettingsPrivate::set(const QString &key, const QVariant &value) { - const std::string keyString = prependStoragePrefix(key).toStdString(); + const std::string keyString = QString(m_keyPrefixes.first() + key).toStdString(); const std::string valueString = QSettingsPrivate::variantToString(value).toStdString(); m_localStorage.call("setItem", keyString, valueString); } std::optional QWasmLocalStorageSettingsPrivate::get(const QString &key) const { - const std::string keyString = prependStoragePrefix(key).toStdString(); - const emscripten::val value = m_localStorage.call("getItem", keyString); - if (value.isNull()) - return std::nullopt; - const QString valueString = QString::fromStdString(value.as()); - return QSettingsPrivate::stringToVariant(valueString); + for (const auto &prefix : m_keyPrefixes) { + const std::string keyString = QString(prefix + key).toStdString(); + const emscripten::val value = m_localStorage.call("getItem", keyString); + if (!value.isNull()) + return QSettingsPrivate::stringToVariant( + QString::fromStdString(value.as())); + if (!fallbacks) + return std::nullopt; + } + return std::nullopt; } QStringList QWasmLocalStorageSettingsPrivate::children(const QString &prefix, ChildSpec spec) const { + QSet 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 = m_localStorage["length"].as(); for (int i = 0; i < length; ++i) { - const QString keyString = - QString::fromStdString(m_localStorage.call("key", i).as()); + for (const auto &storagePrefix : m_keyPrefixes) { + const QString keyString = + QString::fromStdString(m_localStorage.call("key", i).as()); - const QStringView key = removeStoragePrefix(QStringView(keyString)); - if (key.isEmpty()) - continue; - if (!key.startsWith(prefix)) - continue; - - QSettingsPrivate::processChild(key.sliced(prefix.length()), spec, children); + 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 children; + return QStringList(nodes.begin(), nodes.end()); } void QWasmLocalStorageSettingsPrivate::clear() { // Get all Qt keys from window.localStorage const int length = m_localStorage["length"].as(); - std::vector keys; + QStringList keys; keys.reserve(length); - for (int i = 0; i < length; ++i) { - std::string key = (m_localStorage.call("key", i).as()); - keys.push_back(std::move(key)); - } + for (int i = 0; i < length; ++i) + keys.append(QString::fromStdString((m_localStorage.call("key", i).as()))); // 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 (std::string key: keys) { - if (removeStoragePrefix(QString::fromStdString(key)).isEmpty() == false) - m_localStorage.call("removeItem", key); + for (const QString &key : keys) { + if (!keyNameFromPrefixedStorageName(m_keyPrefixes.first(), key).isEmpty()) + m_localStorage.call("removeItem", key.toStdString()); } } @@ -154,19 +206,6 @@ 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 @@ -385,17 +424,37 @@ QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format, QSettings:: format = QSettings::IniFormat; } - // Create settings backend according to selected format - if (format == WebLocalStorageFormat) { + if (format == WebLocalStorageFormat) return new QWasmLocalStorageSettingsPrivate(scope, organization, application); - } else if (format == WebIdbFormat) { + 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; + // Create settings backend according to selected format + switch (format) { + 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: + /* NOTREACHED */ assert(0); + break; + } } QT_END_NAMESPACE diff --git a/tests/auto/corelib/io/qsettings/tst_qsettings.cpp b/tests/auto/corelib/io/qsettings/tst_qsettings.cpp index 2994b32dc16..8aac62a5ca5 100644 --- a/tests/auto/corelib/io/qsettings/tst_qsettings.cpp +++ b/tests/auto/corelib/io/qsettings/tst_qsettings.cpp @@ -41,6 +41,10 @@ #include "qplatformdefs.h" #endif +#if defined(Q_OS_WASM) +#include "emscripten/val.h" +#endif + Q_DECLARE_METATYPE(QSettings::Format) #ifndef QSETTINGS_P_H_VERSION @@ -323,6 +327,9 @@ void tst_QSettings::cleanupTestFiles() QSettings(QSettings::UserScope, "other.software.org").clear(); QSettings(QSettings::SystemScope, "other.software.org").clear(); #endif +#if defined(Q_OS_WASM) + emscripten::val::global("window")["localStorage"].call("clear"); +#endif const QString foo(QLatin1String("foo")); @@ -2050,6 +2057,10 @@ void SettingsThread::run() void tst_QSettings::testThreadSafety() { +#if !QT_CONFIG(thread) + QSKIP("This test requires threads to be enabled."); +#endif // !QT_CONFIG(thread) + SettingsThread threads[NumThreads]; int i, j; @@ -2328,6 +2339,12 @@ void tst_QSettings::fromFile() QStringList strList = QStringList() << "hope" << "destiny" << "chastity"; +#if !defined(Q_OS_WIN) + auto deleteFile = QScopeGuard([path, oldCur]() { + QFile::remove(path); + QDir::setCurrent(oldCur); + }); +#endif // !defined(Q_OS_WIN) { QSettings settings1(path, format); QVERIFY(settings1.allKeys().isEmpty()); @@ -2363,8 +2380,6 @@ void tst_QSettings::fromFile() QCOMPARE(settings1.value("gamma/foo.bar").toInt(), 4); QCOMPARE(settings1.allKeys().size(), 3); } - - QDir::setCurrent(oldCur); } static bool containsSubList(QStringList mom, QStringList son) @@ -3310,7 +3325,7 @@ void tst_QSettings::setPath() path checks that it has no bad side effects. */ for (int i = 0; i < 2; ++i) { -#if !defined(Q_OS_WIN) && !defined(Q_OS_DARWIN) +#if !defined(Q_OS_WIN) && !defined(Q_OS_DARWIN) && !defined(Q_OS_WASM) TEST_PATH(i == 0, "conf", NativeFormat, UserScope, "alpha") TEST_PATH(i == 0, "conf", NativeFormat, SystemScope, "beta") #endif @@ -3427,6 +3442,10 @@ void tst_QSettings::rainersSyncBugOnMac() if (format == QSettings::NativeFormat) QSKIP("Apple OSes do not support direct reads from and writes to .plist files, due to caching and background syncing. See QTBUG-34899."); #endif +#if defined(Q_OS_WASM) + if (format == QSettings::NativeFormat) + QSKIP("WASM's localStorage backend recognizes no concept of file"); +#endif // Q_OS_WASM QString fileName; @@ -3497,14 +3516,14 @@ void tst_QSettings::consistentRegistryStorage() } #endif -#if defined(QT_BUILD_INTERNAL) && defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) && !defined(QT_NO_STANDARDPATHS) +#if defined(QT_BUILD_INTERNAL) && defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) && !defined(Q_OS_WASM) && !defined(QT_NO_STANDARDPATHS) QT_BEGIN_NAMESPACE extern void clearDefaultPaths(); QT_END_NAMESPACE #endif void tst_QSettings::testXdg() { -#if defined(QT_BUILD_INTERNAL) && defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) && !defined(QT_NO_STANDARDPATHS) +#if defined(QT_BUILD_INTERNAL) && defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) && !defined(Q_OS_WASM) && !defined(QT_NO_STANDARDPATHS) // Note: The XDG_CONFIG_DIRS test must be done before overriding the system path // by QSettings::setPath/setSystemIniPath (used in cleanupTestFiles()). clearDefaultPaths();