diff --git a/debian/control b/debian/control index b987b7c4..fd0e1479 100644 --- a/debian/control +++ b/debian/control @@ -46,6 +46,7 @@ Description: Development ToolKit Core Utilities (DTK6 with Qt6) Package: libdtk6core-dev Architecture: any Depends: libdtk6core( =${binary:Version}), + libdtk6core-bin( =${binary:Version}), libdtkcommon-dev(>=5.6.16), libdtk6log-dev Build-Profiles: Description: Development ToolKit Core Devel Library (DTK6 with Qt6) @@ -84,6 +85,7 @@ Description: Development ToolKit Core Utilities (DTK5 with Qt5) Package: libdtkcore-dev Architecture: any Depends: libdtkcore5( =${binary:Version}), + libdtkcore5-bin( =${binary:Version}), libdtkcommon-dev(>=5.6.16), libdtklog-dev Build-Profiles: Description: Development ToolKit Core Devel Library (DTK5 with Qt5) diff --git a/include/global/dconfigfile.h b/include/global/dconfigfile.h index 5959fd9f..6865314f 100644 --- a/include/global/dconfigfile.h +++ b/include/global/dconfigfile.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021 - 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2021 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -35,7 +35,9 @@ class LIBDTKCORESHARED_EXPORT DConfigFile : public DObject{ enum Permissions { ReadOnly, - ReadWrite + ReadWrite, + AuthorizedReadOnly, + AuthorizedReadWrite, }; enum Visibility { diff --git a/src/dconfigfile.cpp b/src/dconfigfile.cpp index af1d5884..d5a9512c 100644 --- a/src/dconfigfile.cpp +++ b/src/dconfigfile.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -386,8 +387,13 @@ class Q_DECL_HIDDEN DConfigInfo { { DConfigFile::Permissions p = DConfigFile::ReadOnly; const auto &tmp = values[key][QLatin1String("permissions")].toString(); - if (tmp == QLatin1String("readwrite")) + if (tmp == QLatin1String("readwrite")) { p = DConfigFile::ReadWrite; + } else if (tmp == QLatin1String("authorizedreadonly")) { + p = DConfigFile::AuthorizedReadOnly; + } else if (tmp == QLatin1String("authorizedreadwrite")) { + p = DConfigFile::AuthorizedReadWrite; + } return p; } @@ -1080,7 +1086,9 @@ DConfigMetaImpl::~DConfigMetaImpl() @~english @fn void setCachePathPrefix(const QString &prefix) = 0; @brief Set cache's prefix path, it's access permissions is considered by caller, -and it needs to distinguish the paths of different caches by caller. +and it needs to distinguish the paths of different caches by caller. When a +prefix is set, load the legacy cache first and then overlay the cache stored +under the prefix. Saving always uses the prefix path. @param prefix cache's prefix path. */ @@ -1106,9 +1114,10 @@ class Q_DECL_HIDDEN DConfigCacheImpl : public DConfigCache { return values.keyList(); } - inline QString applicationCacheDir(const QString &localPrefix, const QString &suffix) const + inline QString applicationCacheDir(const QString &localPrefix, const QString &suffix, + const QString &pathPrefix) const { - QString prefix(cachePrefix); + QString prefix(pathPrefix); if (prefix.isEmpty()) { // If target user is current user or system user, then get the home path by environment variable first. QString homePath; @@ -1129,7 +1138,7 @@ class Q_DECL_HIDDEN DConfigCacheImpl : public DConfigCache { inline QString applicationCacheDir(const QString &localPrefix) const { - return applicationCacheDir(localPrefix, QString()); + return applicationCacheDir(localPrefix, QString(), cachePrefix); } inline QString cacheDir(const QString &basePath) { @@ -1137,8 +1146,9 @@ class Q_DECL_HIDDEN DConfigCacheImpl : public DConfigCache { return dir.filePath(configKey.fileName + FILE_SUFFIX); } - inline QString globalCacheDir(const QString &localPrefix) const { - QString prefix(cachePrefix); + inline QString globalCacheDir(const QString &localPrefix, const QString &pathPrefix) const + { + QString prefix(pathPrefix); if (prefix.isEmpty()) { // TODO `DSG_APP_DATA` is not set and `appid` is not captured in `DStandardPaths::path`. QString appDataDir = DStandardPaths::path(DStandardPaths::DSG::AppData); @@ -1161,20 +1171,33 @@ class Q_DECL_HIDDEN DConfigCacheImpl : public DConfigCache { return QDir::cleanPath(QString("%1/%2/%3").arg(localPrefix, prefix, configKey.appId)); } - QString getCacheDir(const QString &localPrefix = QString()) + QString getCacheDir(const QString &localPrefix, const QString &pathPrefix) const { if (isGlobal()) { - const QString &dir = globalCacheDir(localPrefix); + const QString &dir = globalCacheDir(localPrefix, pathPrefix); if (!dir.isEmpty()) return dir; // Not supported the global config, fallback the config cache data to user directory. - return applicationCacheDir(localPrefix, "-fake-global"); + return applicationCacheDir(localPrefix, "-fake-global", pathPrefix); } else { - return applicationCacheDir(localPrefix); + return applicationCacheDir(localPrefix, QString(), pathPrefix); } } + QString getCacheDir(const QString &localPrefix = QString()) const + { + return getCacheDir(localPrefix, cachePrefix); + } + + enum class CacheLoadStatus { + NotFound, + Loaded, + Invalid + }; + + CacheLoadStatus loadCache(const QString &dir, QSet *loadedKeys = nullptr); + bool load(const QString &localPrefix = QString()) override; bool isGlobal() const override @@ -1235,18 +1258,52 @@ DConfigCacheImpl::~DConfigCacheImpl() bool DConfigCacheImpl::load(const QString &localPrefix) { - // cache 文件要严格匹配 subpath - const QString &dir = getCacheDir(localPrefix); - if (dir.isEmpty()) { - return true; + const QString newCacheDir = getCacheDir(localPrefix); + if (cachePrefix.isEmpty()) + return loadCache(newCacheDir) != CacheLoadStatus::Invalid; + + const QString legacyCacheDir = getCacheDir(localPrefix, QString()); + if (legacyCacheDir == newCacheDir) + return loadCache(newCacheDir) != CacheLoadStatus::Invalid; + + QSet legacyKeys; + const auto legacyStatus = loadCache(legacyCacheDir, &legacyKeys); + if (legacyStatus == CacheLoadStatus::Invalid) { + qCWarning(cfLog, "Failed to load legacy cache from \"%s\"; continue with the new cache.", + qPrintable(legacyCacheDir)); + legacyKeys.clear(); + } + + QSet newKeys; + const auto newStatus = loadCache(newCacheDir, &newKeys); + if (newStatus == CacheLoadStatus::Invalid) + return false; + + if (legacyStatus == CacheLoadStatus::Loaded) { + for (const QString &key : legacyKeys) { + if (!newKeys.contains(key)) { + cacheChanged = true; + break; + } + } } + + return true; +} + +DConfigCacheImpl::CacheLoadStatus DConfigCacheImpl::loadCache(const QString &dir, + QSet *loadedKeys) +{ + // cache 文件要严格匹配 subpath + if (dir.isEmpty()) + return CacheLoadStatus::NotFound; + QScopedPointer cache(loadFile(dir, configKey.subpath, configKey.fileName + FILE_SUFFIX, false)); - if (!cache) { - return true; - } + if (!cache) + return CacheLoadStatus::NotFound; const JsonParseResult pr = loadJsonFile(cache.data()); const QJsonDocument &doc = pr.doc; @@ -1254,9 +1311,9 @@ bool DConfigCacheImpl::load(const QString &localPrefix) if (doc.isObject()) { const QJsonObject &root = doc.object(); if (!checkMagic(root, MAGIC_CACHE)) - return false; + return CacheLoadStatus::Invalid; if (!checkVersion(root, DConfigFile::supportedVersion())) - return false; + return CacheLoadStatus::Invalid; auto &&contents = root[QLatin1String("contents")].toObject(); auto i = contents.constBegin(); @@ -1273,9 +1330,11 @@ bool DConfigCacheImpl::load(const QString &localPrefix) jsonValueToVariant(valueField, isOriginalValueFloat(pr.raw, i.key(), valueField))); } values.update(i.key(), vh); + if (loadedKeys) + loadedKeys->insert(i.key()); } } - return true; + return CacheLoadStatus::Loaded; } bool DConfigCacheImpl::save(const QString &localPrefix, QJsonDocument::JsonFormat format, bool sync) diff --git a/tests/ut_dconfigfile.cpp b/tests/ut_dconfigfile.cpp index d00a231c..23227832 100644 --- a/tests/ut_dconfigfile.cpp +++ b/tests/ut_dconfigfile.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "test_helper.hpp" @@ -96,6 +97,35 @@ TEST_F(ut_DConfigFile, testLoad) { ASSERT_EQ(config.meta()->permissions("canExit"), DConfigFile::ReadWrite); } +TEST_F(ut_DConfigFile, authorizedPermissions) { + QByteArray meta = R"delimiter( +{ + "magic": "dsg.config.meta", + "version": "1.0", + "contents": { + "authorizedReadOnly": { + "value": true, + "serial": 0, + "permissions": "authorizedreadonly" + }, + "authorizedReadWrite": { + "value": true, + "serial": 0, + "permissions": "authorizedreadwrite" + } + } +} + )delimiter"; + + QBuffer buffer(&meta); + DConfigFile config(APP_ID, FILE_NAME); + ASSERT_TRUE(config.load(&buffer, {})); + ASSERT_EQ(config.meta()->permissions("authorizedReadOnly"), + DConfigFile::AuthorizedReadOnly); + ASSERT_EQ(config.meta()->permissions("authorizedReadWrite"), + DConfigFile::AuthorizedReadWrite); +} + TEST_F(ut_DConfigFile, setValueTypeCheck) { FileCopyGuard guard(":/data/dconf-example.meta.json", QString("%1/%2.json").arg(metaPath, FILE_NAME)); @@ -480,6 +510,143 @@ TEST_F(ut_DConfigFile, setCachePathPrefix) { } } +TEST_F(ut_DConfigFile, cachePathPrefixOverlaysNewCacheOnLegacyCache) { + FileCopyGuard guard(":/data/dconf-example.meta.json", + QString("%1/%2.json").arg(metaPath, FILE_NAME)); + const QString newPrefix("/configs-user-overlay"); + + // Create the new cache first so it does not inherit any legacy values. + { + DConfigFile config(APP_ID, FILE_NAME); + ASSERT_TRUE(config.load(LocalPrefix)); + QScopedPointer cache(config.createUserCache(uid)); + cache->setCachePathPrefix(newPrefix); + ASSERT_TRUE(cache->load(LocalPrefix)); + ASSERT_TRUE(config.setValue("key2", "new-value", "test", cache.get())); + ASSERT_TRUE(cache->save(LocalPrefix)); + } + + // The legacy cache contains one conflicting key and one legacy-only key. + { + DConfigFile config(APP_ID, FILE_NAME); + ASSERT_TRUE(config.load(LocalPrefix)); + QScopedPointer cache(config.createUserCache(uid)); + ASSERT_TRUE(cache->load(LocalPrefix)); + ASSERT_TRUE(config.setValue("key2", "legacy-value", "test", cache.get())); + ASSERT_TRUE(config.setValue("readwrite", false, "test", cache.get())); + ASSERT_TRUE(cache->save(LocalPrefix)); + } + + { + DConfigFile config(APP_ID, FILE_NAME); + ASSERT_TRUE(config.load(LocalPrefix)); + QScopedPointer cache(config.createUserCache(uid)); + cache->setCachePathPrefix(newPrefix); + ASSERT_TRUE(cache->load(LocalPrefix)); + ASSERT_EQ(config.value("key2", cache.get()), QString("new-value")); + ASSERT_EQ(config.value("readwrite", cache.get()), false); + + // load() marks the merged cache dirty, so the existing save path + // persists the legacy-only value into the new cache. + ASSERT_TRUE(cache->save(LocalPrefix)); + } + + const QString legacyCache = QDir::cleanPath( + QString("%1/tmp/home/.config/dsg/configs/%2/%3.json") + .arg(LocalPrefix, APP_ID, FILE_NAME)); + ASSERT_TRUE(QFile::remove(legacyCache)); + + // The legacy-only value remains available from the saved new cache. + { + DConfigFile config(APP_ID, FILE_NAME); + ASSERT_TRUE(config.load(LocalPrefix)); + QScopedPointer cache(config.createUserCache(uid)); + cache->setCachePathPrefix(newPrefix); + ASSERT_TRUE(cache->load(LocalPrefix)); + ASSERT_EQ(config.value("key2", cache.get()), QString("new-value")); + ASSERT_EQ(config.value("readwrite", cache.get()), false); + } +} + +TEST_F(ut_DConfigFile, cachePathPrefixOverlaysGlobalCache) { + FileCopyGuard guard(":/data/dconf-example.meta.json", + QString("%1/%2.json").arg(metaPath, FILE_NAME)); + const QString newPrefix("/configs-global-overlay"); + + { + DConfigFile config(APP_ID, FILE_NAME); + config.globalCache()->setCachePathPrefix(newPrefix); + ASSERT_TRUE(config.load(LocalPrefix)); + ASSERT_TRUE(config.setValue("key3", "new-global", "test")); + ASSERT_TRUE(config.save(LocalPrefix)); + } + + { + DConfigFile config(APP_ID, FILE_NAME); + ASSERT_TRUE(config.load(LocalPrefix)); + ASSERT_TRUE(config.setValue("key3", "legacy-global", "test")); + ASSERT_TRUE(config.setValue("number", 42, "test")); + ASSERT_TRUE(config.save(LocalPrefix)); + } + + { + DConfigFile config(APP_ID, FILE_NAME); + config.globalCache()->setCachePathPrefix(newPrefix); + ASSERT_TRUE(config.load(LocalPrefix)); + ASSERT_EQ(config.value("key3"), QString("new-global")); + ASSERT_EQ(config.value("number"), 42); + ASSERT_TRUE(config.save(LocalPrefix)); + } + + const QString newCache = QDir::cleanPath( + QString("%1/%2/%3/%4.json").arg(LocalPrefix, newPrefix, APP_ID, FILE_NAME)); + QFile cacheFile(newCache); + ASSERT_TRUE(cacheFile.open(QIODevice::ReadOnly)); + const QJsonObject contents = QJsonDocument::fromJson(cacheFile.readAll()) + .object() + .value("contents") + .toObject(); + ASSERT_EQ(contents.value("key3").toObject().value("value").toString(), + QString("new-global")); + ASSERT_EQ(contents.value("number").toObject().value("value").toInt(), 42); +} + +TEST_F(ut_DConfigFile, cachePathPrefixSupportsNoAppIdAndSubpath) { + FileCopyGuard guard(":/data/dconf-example.meta.json", + QString("%1/%2.json").arg(noAppidMetaPath, FILE_NAME)); + const QString newPrefix("/configs-no-appid-overlay"); + const QString subpath("/nested/path"); + + { + DConfigFile config(NoAppId, FILE_NAME, subpath); + ASSERT_TRUE(config.load(LocalPrefix)); + QScopedPointer cache(config.createUserCache(uid)); + cache->setCachePathPrefix(newPrefix); + ASSERT_TRUE(cache->load(LocalPrefix)); + ASSERT_TRUE(config.setValue("readwrite", false, "test", cache.get())); + ASSERT_TRUE(cache->save(LocalPrefix)); + } + + { + DConfigFile config(NoAppId, FILE_NAME, subpath); + ASSERT_TRUE(config.load(LocalPrefix)); + QScopedPointer cache(config.createUserCache(uid)); + ASSERT_TRUE(cache->load(LocalPrefix)); + ASSERT_TRUE(config.setValue("key2", "legacy-no-appid", "test", cache.get())); + ASSERT_TRUE(cache->save(LocalPrefix)); + } + + { + DConfigFile config(NoAppId, FILE_NAME, subpath); + ASSERT_TRUE(config.load(LocalPrefix)); + QScopedPointer cache(config.createUserCache(uid)); + cache->setCachePathPrefix(newPrefix); + ASSERT_TRUE(cache->load(LocalPrefix)); + ASSERT_EQ(config.value("readwrite", cache.get()), false); + ASSERT_EQ(config.value("key2", cache.get()), QString("legacy-no-appid")); + } +} + TEST_F(ut_DConfigFile, setSubpath) { FileCopyGuard guard(":/data/dconf-example.meta.json", QString("%1/%2.json").arg(metaPath, FILE_NAME));