From e9dc7174b094ac263bb02bddaa67189df6a544c5 Mon Sep 17 00:00:00 2001 From: yeshanshan Date: Fri, 10 Jul 2026 18:37:24 +0800 Subject: [PATCH] fix: preserve float literal type in JSON config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. QJsonValue::toVariant() converts all JSON numbers to double internally, but when converting to QVariant, it degrades floating-point numbers without fractional parts (e.g., 1.0) to integers (qlonglong), losing the original float intent 2. Added JsonParseResult struct to keep raw JSON bytes alongside parsed document for float-literal detection 3. Added isOriginalValueFloat() helper that checks raw JSON text for '.' or 'e'/'E' characters in number literals 4. Added jsonValueToVariant() helper that converts QJsonValue to QVariant with forced double type when the raw literal was a float 5. Updated loadJsonFile() to return JsonParseResult with raw bytes 6. Applied float type preservation in both meta file loading and override loading paths 7. Updated unit test to verify that default values with float literals (like "1.0") remain as QMetaType::Double, and integer literals (like "1") remain as QMetaType::LongLong Log: Fixed issue where JSON config values with float literals (e.g., 1.0) were incorrectly degraded to integer types Influence: 1. Test config values with float literals (1.0, 2.5, 1e10) to ensure they remain double type 2. Test config values with integer literals (1, 100) to ensure they remain integer type 3. Verify override files with float values are correctly applied 4. Test backward compatibility with existing config files 5. Verify dde-dconfig set command accepts float values correctly 6. Test edge cases: negative floats, scientific notation, values without decimal part fix: 修复 JSON 配置值中浮点数字面量的类型保留问题 1. QJsonValue::toVariant() 将所有 JSON 数字转换为 double 类型,但 在转换为 QVariant 时,会将没有小数部分的浮点数(如 1.0)退化为整数 (qlonglong),丢失原始的浮点意图 2. 添加了 JsonParseResult 结构体,用于在解析文档时保留原始 JSON 字节数 据,以便进行浮点数字面量检测 3. 添加了 isOriginalValueFloat() 辅助函数,通过检查原始 JSON 文本中数字 字面量是否包含 '.' 或 'e'/'E' 字符来判断是否为浮点数 4. 添加了 jsonValueToVariant() 辅助函数,在将 QJsonValue 转换为 QVariant 时,如果原始字面量为浮点数则强制保留 double 类型 5. 更新了 loadJsonFile() 函数使其返回 JsonParseResult(包含原始字节 数据) 6. 在元文件加载和覆盖文件加载路径中都应用了浮点类型保留逻辑 7. 更新了单元测试,验证包含浮点字面量的默认值(如 "1.0")保持为 QMetaType::Double,整数字面量(如 "1")保持为 QMetaType::LongLong Log: 修复了 JSON 配置值中浮点数字面量(如 1.0)被错误退化为整数类型的 问题 Influence: 1. 测试包含浮点字面量的配置值(1.0, 2.5, 1e10)确保保持 double 类型 2. 测试包含整数字面量的配置值(1, 100)确保保持整数类型 3. 验证包含浮点值的覆盖文件是否正确应用 4. 测试与现有配置文件的向后兼容性 5. 验证 dde-dconfig set 命令是否正确接受浮点值 6. 测试边界情况:负浮点数、科学计数法、不包含小数部分的值 --- src/dconfigfile.cpp | 107 +++++++++++++++++++++++++++++++++------ tests/ut_dconfigfile.cpp | 20 ++++++-- 2 files changed, 108 insertions(+), 19 deletions(-) diff --git a/src/dconfigfile.cpp b/src/dconfigfile.cpp index d40a7e18..af1d5884 100644 --- a/src/dconfigfile.cpp +++ b/src/dconfigfile.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,50 @@ Q_LOGGING_CATEGORY(cfLog, "dtk.dsg.config"); #define FILE_SUFFIX QLatin1String(".json") +// QJson treats all numbers as double, and QJsonValue::toVariant() degrades a +// float literal with no fractional part (e.g. 1.0) to an integer type +// (qlonglong). To keep the original float intent, we match the raw JSON text +// to detect whether the "value" literal contained '.' or 'e'/'E'. +// This mirrors the approach used by tools/dconfig2cpp/main.cpp:isOriginalValueFloat. +struct JsonParseResult { + QJsonDocument doc; + QByteArray raw; // raw bytes, kept for float-literal detection + bool isValid() const { return !doc.isNull(); } +}; + +// Check whether the number literal under "propertyName" -> {"value": ...} +// in the raw JSON is a floating-point literal (contains '.' or 'e'/'E'). +static bool isOriginalValueFloat(const QByteArray &raw, const QString &propertyName, const QJsonValue &value) +{ + if (!value.isDouble()) { + return false; + } + const QString searchPattern = QString("\"%1\"\\s*:\\s*\\{[^}]*\"value\"\\s*:\\s*([0-9.eE+-]+)").arg(propertyName); + const QRegularExpression regex(searchPattern); + const QRegularExpressionMatch match = regex.match(QString::fromUtf8(raw)); + if (match.hasMatch()) { + const QString numberStr = match.captured(1); + return numberStr.contains(QLatin1Char('.')) || numberStr.contains(QLatin1Char('e'), Qt::CaseInsensitive); + } + return false; +} + +// Convert a QJsonValue to QVariant, forcing double when the raw literal was float. +inline static QVariant jsonValueToVariant(const QJsonValue &value, bool isFloat) +{ + QVariant var = value.toVariant(); + if (isFloat) { + QVariant copy = var; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (copy.convert(QMetaType(QMetaType::Double))) +#else + if (copy.convert(QVariant::Double)) +#endif + var = copy; + } + return var; +} + // subpath must be a subdirectory of the dir. inline static bool subpathIsValid(const QString &subpath, const QDir &dir) { @@ -111,26 +156,29 @@ inline QFile *loadFile(const QString &baseDir, const QString &subpath, const QSt return nullptr; } -static QJsonDocument loadJsonFile(QIODevice *data) +static JsonParseResult loadJsonFile(QIODevice *data) { if (!data->open(QIODevice::ReadOnly)) { if (auto file = qobject_cast(data)) { qCDebug(cfLog, "Falied on open file: \"%s\", error message: \"%s\"", qPrintable(file->fileName()), qPrintable(file->errorString())); } - return QJsonDocument(); + return JsonParseResult(); } - QJsonParseError error; - auto document = QJsonDocument::fromJson(data->readAll(), &error); + JsonParseResult result; + result.raw = data->readAll(); data->close(); + QJsonParseError error; + result.doc = QJsonDocument::fromJson(result.raw, &error); + if (error.error != QJsonParseError::NoError) { qCWarning(cfLog, "%s", qPrintable(error.errorString())); - return QJsonDocument(); + return JsonParseResult(); } - return document; + return result; } static DConfigFile::Version parseVersion(const QJsonObject &obj) { @@ -443,9 +491,9 @@ class Q_DECL_HIDDEN DConfigInfo { return true; } - inline bool updateValue(const QString &key, const QJsonValue &value) + inline bool updateValue(const QString &key, const QJsonValue &value, const QByteArray &raw) { - return overrideValue(key, "value", value); + return overrideValue(key, "value", value, raw); } inline void updateSerial(const QString &key, const QJsonValue &value) @@ -467,14 +515,20 @@ class Q_DECL_HIDDEN DConfigInfo { return contents; } private: - bool overrideValue(const QString &key, const QString &subkey, const QJsonValue &from) { + bool overrideValue(const QString &key, const QString &subkey, const QJsonValue &from, const QByteArray &raw = QByteArray()) { const QJsonValue &v = from[subkey]; if (v.isUndefined()) { return false; } - values[key][subkey] = v.toVariant(); + if (subkey == QLatin1String("value")) { + // QJsonValue::toVariant() degrades 1.0 to qlonglong; restore double + // when the raw literal was a float. + values[key][subkey] = jsonValueToVariant(v, isOriginalValueFloat(raw, key, v)); + } else { + values[key][subkey] = v.toVariant(); + } return true; } @@ -732,7 +786,8 @@ class Q_DECL_HIDDEN DConfigMetaImpl : public DConfigMeta { bool load(QIODevice *meta, const QList &overrides) override { { - const QJsonDocument &doc = loadJsonFile(meta); + const JsonParseResult pr = loadJsonFile(meta); + const QJsonDocument &doc = pr.doc; if (!doc.isObject()) return false; @@ -759,7 +814,16 @@ class Q_DECL_HIDDEN DConfigMetaImpl : public DConfigMeta { // 初始化原始值 for (; i != contents.constEnd(); ++i) { - if (!values.update(i.key(), i.value().toObject().toVariantHash())) { + const QJsonObject itemObj = i.value().toObject(); + QVariantHash vh = itemObj.toVariantHash(); + // QJsonValue::toVariant() degrades 1.0 to qlonglong; restore double + // when the raw literal was a float. + const QJsonValue valueField = itemObj.value(QLatin1String("value")); + if (valueField.isDouble()) { + vh.insert(QLatin1String("value"), + jsonValueToVariant(valueField, isOriginalValueFloat(pr.raw, i.key(), valueField))); + } + if (!values.update(i.key(), vh)) { qWarning() << "key:" << i.key() << "has no value"; return false; } @@ -767,7 +831,8 @@ class Q_DECL_HIDDEN DConfigMetaImpl : public DConfigMeta { } // for override Q_FOREACH(auto override, overrides) { - const QJsonDocument &doc = loadJsonFile(override); + const JsonParseResult ovr = loadJsonFile(override); + const QJsonDocument &doc = ovr.doc; if (doc.isObject()) { const QJsonObject &root = doc.object(); if (!checkMagic(root, MAGIC_OVERRIDE)) { @@ -800,7 +865,7 @@ class Q_DECL_HIDDEN DConfigMetaImpl : public DConfigMeta { if (values.flags(i.key()) & DConfigFile::NoOverride) continue; - if (!values.updateValue(i.key(), i.value())) { + if (!values.updateValue(i.key(), i.value(), ovr.raw)) { qWarning() << "key (override):" << i.key() << "has no value"; return false; } @@ -1183,7 +1248,8 @@ bool DConfigCacheImpl::load(const QString &localPrefix) return true; } - const QJsonDocument &doc = loadJsonFile(cache.data()); + const JsonParseResult pr = loadJsonFile(cache.data()); + const QJsonDocument &doc = pr.doc; if (doc.isObject()) { const QJsonObject &root = doc.object(); @@ -1197,7 +1263,16 @@ bool DConfigCacheImpl::load(const QString &localPrefix) // 原样保存原始数据 for (; i != contents.constEnd(); ++i) { - values.update(i.key(), i.value().toObject().toVariantHash()); + const QJsonObject itemObj = i.value().toObject(); + QVariantHash vh = itemObj.toVariantHash(); + // QJsonValue::toVariant() degrades 1.0 to qlonglong; restore double + // when the raw literal was a float. + const QJsonValue valueField = itemObj.value(QLatin1String("value")); + if (valueField.isDouble()) { + vh.insert(QLatin1String("value"), + jsonValueToVariant(valueField, isOriginalValueFloat(pr.raw, i.key(), valueField))); + } + values.update(i.key(), vh); } } return true; diff --git a/tests/ut_dconfigfile.cpp b/tests/ut_dconfigfile.cpp index 8938709b..d00a231c 100644 --- a/tests/ut_dconfigfile.cpp +++ b/tests/ut_dconfigfile.cpp @@ -125,9 +125,23 @@ TEST_F(ut_DConfigFile, setValueTypeCheck) { ASSERT_EQ(config.value("number", userCache.get()).type(), type); } { - const auto type = config.value("numberDouble", userCache.get()).type(); - ASSERT_TRUE(config.setValue("numberDouble", 1.2, "test", userCache.get())); - ASSERT_EQ(config.value("numberDouble", userCache.get()), 1.2); + const auto type = config.value("numberDouble", userCache.get()).type(); + ASSERT_TRUE(config.setValue("numberDouble", 1.2, "test", userCache.get())); + ASSERT_EQ(config.value("numberDouble", userCache.get()), 1.2); + } + // The default literal in meta is "1.0" (a float literal); toVariant() + // would normally degrade it to qlonglong, losing the float intent. With + // the raw-text float detection fix, the default must stay double so that + // a later `dde-dconfig set -v 1.3` takes the Double branch. + { + const auto defaultValue = config.value("numberDouble", userCache.get()); + ASSERT_EQ(defaultValue.userType(), static_cast(QMetaType::Double)); + } + // Conversely, "number" has default literal "1" (no fractional part), so + // it must remain an integer. + { + const auto defaultValue = config.value("number", userCache.get()); + ASSERT_EQ(defaultValue.userType(), static_cast(QMetaType::LongLong)); } { const auto type = config.value("array", userCache.get()).type();