diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-01 23:12:44 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-01 23:12:44 -0500 |
| commit | 4a15d47e96ff5870bb9a1c2f52a7c41e4bf4b63b (patch) | |
| tree | e56d8c9e6eb4752175a0f1ec5864107a63554f2a /src | |
| parent | 1f198921b2c1f23f132d25552cacb650af253d15 (diff) | |
meta.ini: revert to upstream CamelCase, canonicalize on read
Restore upstream MO2's exact key cases (gameName, installationFile,
nexusDescription, etc.) in readMeta/saveMeta/doInstall/createMod —
matches mod-shipped meta.ini cases, so on Linux Qt6's case-sensitive
QSettings IniFormat, setValue updates in place without producing a
duplicate key.
normalizeMetaIniCase() repurposed: instead of folding everything to
lowercase (which mismatched the upstream-style setValue calls and
caused the very dupes it was trying to fix), it now canonicalizes
known per-mod meta.ini keys to their upstream CamelCase, deduping
case-insensitive collisions per section. Cleans up dirty files
written during the 5d1fb29 -> 1f19892 window so they collapse to a
single canonical key per setting on first read.
Tests updated to assert canonical-CamelCase output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/installationmanager.cpp | 12 | ||||
| -rw-r--r-- | src/src/metainiutils.cpp | 71 | ||||
| -rw-r--r-- | src/src/metainiutils.h | 20 | ||||
| -rw-r--r-- | src/src/modinforegular.cpp | 52 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 4 | ||||
| -rw-r--r-- | src/tests/test_metainiutils.cpp | 148 |
6 files changed, 173 insertions, 134 deletions
diff --git a/src/src/installationmanager.cpp b/src/src/installationmanager.cpp index 09e2a36..3e58a2a 100644 --- a/src/src/installationmanager.cpp +++ b/src/src/installationmanager.cpp @@ -549,8 +549,8 @@ InstallationResult InstallationManager::doInstall(GuessedValue<QString>& modName // overwrite settings only if they are actually are available or haven't been set // before - if ((gameName != "") || !settingsFile.contains("gamename")) { - settingsFile.setValue("gamename", gameName); + if ((gameName != "") || !settingsFile.contains("gameName")) { + settingsFile.setValue("gameName", gameName); } if ((modID != 0) || !settingsFile.contains("modid")) { settingsFile.setValue("modid", modID); @@ -561,15 +561,15 @@ InstallationResult InstallationManager::doInstall(GuessedValue<QString>& modName VersionInfo(settingsFile.value("version").toString()))))) { settingsFile.setValue("version", version); } - if (!newestVersion.isEmpty() || !settingsFile.contains("newestversion")) { - settingsFile.setValue("newestversion", newestVersion); + if (!newestVersion.isEmpty() || !settingsFile.contains("newestVersion")) { + settingsFile.setValue("newestVersion", newestVersion); } // issue #51 used to overwrite the manually set categories if (!settingsFile.contains("category")) { settingsFile.setValue("category", QString::number(categoryID)); } - settingsFile.setValue("nexusfilestatus", fileCategoryID); - settingsFile.setValue("installationfile", storeMetaPath(m_CurrentFile)); + settingsFile.setValue("nexusFileStatus", fileCategoryID); + settingsFile.setValue("installationFile", storeMetaPath(m_CurrentFile)); settingsFile.setValue("repository", repository); if (!merge) { diff --git a/src/src/metainiutils.cpp b/src/src/metainiutils.cpp index 7af170f..4ba38b6 100644 --- a/src/src/metainiutils.cpp +++ b/src/src/metainiutils.cpp @@ -24,6 +24,54 @@ bool endsInOddBackslashes(const QByteArray& line) return (n % 2) == 1; } +// Canonical case for keys that appear in per-mod meta.ini, taken from +// upstream MO2's setValue/value calls in modinforegular.cpp, +// installationmanager.cpp, and organizercore.cpp. +const QHash<QByteArray, QByteArray>& canonicalKeyMap() +{ + static const QHash<QByteArray, QByteArray> map = { + {"author", "author"}, + {"category", "category"}, + {"color", "color"}, + {"comments", "comments"}, + {"converted", "converted"}, + {"endorsed", "endorsed"}, + {"fileid", "fileid"}, + {"gamename", "gameName"}, + {"hascustomurl", "hasCustomURL"}, + {"ignoredversion", "ignoredVersion"}, + {"installationfile", "installationFile"}, + {"lastnexusquery", "lastNexusQuery"}, + {"lastnexusupdate", "lastNexusUpdate"}, + {"modid", "modid"}, + {"newestversion", "newestVersion"}, + {"nexuscategory", "nexusCategory"}, + {"nexusdescription", "nexusDescription"}, + {"nexusfilestatus", "nexusFileStatus"}, + {"nexuslastmodified", "nexusLastModified"}, + {"notes", "notes"}, + {"repository", "repository"}, + {"tracked", "tracked"}, + {"uploader", "uploader"}, + {"uploaderurl", "uploaderUrl"}, + {"url", "url"}, + {"validated", "validated"}, + {"version", "version"}, + }; + return map; +} + +// Returns the canonical case for `key` if known, otherwise `key` unchanged. +QByteArray canonicalize(const QByteArray& key) +{ + const auto& map = canonicalKeyMap(); + const auto it = map.find(key.toLower()); + if (it != map.end()) { + return it.value(); + } + return key; +} + } // namespace bool normalizeMetaIniCase(const QString& path) @@ -52,7 +100,6 @@ bool normalizeMetaIniCase(const QString& path) struct Entry { bool isKey = false; - QByteArray key; // lowercased QByteArray rawLine; // for non-key lines QByteArray fullKeyLine; // for key lines, possibly multi-line via \\\n }; @@ -61,7 +108,7 @@ bool normalizeMetaIniCase(const QString& path) { QByteArray header; // [Name] line, empty for the implicit pre-header section QList<Entry> entries; - QHash<QByteArray, qsizetype> indexByKey; + QHash<QByteArray, qsizetype> indexByLowerKey; }; QList<Section> sections; @@ -104,21 +151,21 @@ bool normalizeMetaIniCase(const QString& path) full.append(rawLines[i]); } - const int fullEq = full.indexOf('='); - const QByteArray rawKey = full.left(fullEq).trimmed(); - const QByteArray valuePart = full.mid(fullEq); // includes '=' - const QByteArray lowerKey = rawKey.toLower(); - if (rawKey != lowerKey) { + const int fullEq = full.indexOf('='); + const QByteArray rawKey = full.left(fullEq).trimmed(); + const QByteArray valuePart = full.mid(fullEq); // includes '=' + const QByteArray lowerKey = rawKey.toLower(); + const QByteArray canonicalKey = canonicalize(rawKey); + if (rawKey != canonicalKey) { changed = true; } Entry e; e.isKey = true; - e.key = lowerKey; - e.fullKeyLine = lowerKey + valuePart; + e.fullKeyLine = canonicalKey + valuePart; - auto it = cur.indexByKey.find(lowerKey); - if (it != cur.indexByKey.end()) { + auto it = cur.indexByLowerKey.find(lowerKey); + if (it != cur.indexByLowerKey.end()) { Entry& prev = cur.entries[it.value()]; const QByteArray newVal = valuePart.size() > 1 ? valuePart.mid(1).trimmed() : QByteArray(); @@ -135,7 +182,7 @@ bool normalizeMetaIniCase(const QString& path) changed = true; } else { cur.entries.push_back(std::move(e)); - cur.indexByKey.insert(lowerKey, cur.entries.size() - 1); + cur.indexByLowerKey.insert(lowerKey, cur.entries.size() - 1); } } sections.push_back(std::move(cur)); diff --git a/src/src/metainiutils.h b/src/src/metainiutils.h index c043742..0086747 100644 --- a/src/src/metainiutils.h +++ b/src/src/metainiutils.h @@ -6,17 +6,19 @@ namespace MetaIniUtils { -// Pre-normalize a Qt INI file so QSettings (case-sensitive in IniFormat on -// Linux/Qt6) does not produce duplicate-cased keys when `setValue("foo", x)` -// is invoked on a file already containing `Foo=...`. +// Canonicalize the keys in a per-mod meta.ini to the case used by upstream +// MO2's setValue/value calls (e.g. "gameName", "installationFile"). // -// Lowercases all keys, deduplicates per-section (keeping the latest -// non-empty value), and preserves comments, blank lines, and ordering of -// surviving keys. Multi-line values (trailing `\` continuations) are -// preserved as a single logical entry. +// Qt6's QSettings IniFormat is case-sensitive on Linux, so a meta.ini +// containing both "gameName=" and "gamename=" surfaces as two distinct +// keys. This helper folds case-insensitively-equal keys to a single entry +// using the canonical CamelCase, keeping the non-empty value when one is +// blank, otherwise the later occurrence wins. // -// No-op if the file does not exist or has no case mismatches / duplicates. -// Returns true if the file was modified. +// Unknown keys keep their original case and are still deduped. +// +// No-op if the file does not exist, is empty, or contains no case +// mismatches / duplicates. Returns true if the file was modified. bool normalizeMetaIniCase(const QString& path); } // namespace MetaIniUtils diff --git a/src/src/modinforegular.cpp b/src/src/modinforegular.cpp index 3978f2e..c59706d 100644 --- a/src/src/modinforegular.cpp +++ b/src/src/modinforegular.cpp @@ -113,21 +113,21 @@ void ModInfoRegular::readMeta() QSettings metaFile(metaPath, QSettings::IniFormat); m_Comments = metaFile.value("comments", "").toString(); m_Notes = metaFile.value("notes", "").toString(); - QString const tempGameName = metaFile.value("gamename", m_GameName).toString(); + QString const tempGameName = metaFile.value("gameName", m_GameName).toString(); if (tempGameName.size()) m_GameName = tempGameName; m_NexusID = metaFile.value("modid", -1).toInt(); m_Version.parse(metaFile.value("version", "").toString()); - m_NewestVersion = metaFile.value("newestversion", "").toString(); - m_IgnoredVersion = metaFile.value("ignoredversion", "").toString(); + m_NewestVersion = metaFile.value("newestVersion", "").toString(); + m_IgnoredVersion = metaFile.value("ignoredVersion", "").toString(); m_InstallationFile = - loadMetaPath(metaFile.value("installationfile", "").toString()); - m_NexusDescription = metaFile.value("nexusdescription", "").toString(); - m_NexusFileStatus = metaFile.value("nexusfilestatus", "1").toInt(); - m_NexusCategory = metaFile.value("nexuscategory", 0).toInt(); + loadMetaPath(metaFile.value("installationFile", "").toString()); + m_NexusDescription = metaFile.value("nexusDescription", "").toString(); + m_NexusFileStatus = metaFile.value("nexusFileStatus", "1").toInt(); + m_NexusCategory = metaFile.value("nexusCategory", 0).toInt(); m_Author = metaFile.value("author", "").toString(); m_Uploader = metaFile.value("uploader", "").toString(); - m_UploaderUrl = metaFile.value("uploaderurl", "").toString(); + m_UploaderUrl = metaFile.value("uploaderUrl", "").toString(); m_Repository = metaFile.value("repository", "Nexus").toString(); m_Converted = metaFile.value("converted", false).toBool(); m_Validated = metaFile.value("validated", false).toBool(); @@ -181,8 +181,8 @@ void ModInfoRegular::readMeta() // always read the url m_CustomURL = metaFile.value("url").toString(); - if (metaFile.contains("hascustomurl")) { - m_HasCustomURL = metaFile.value("hascustomurl").toBool(); + if (metaFile.contains("hasCustomURL")) { + m_HasCustomURL = metaFile.value("hasCustomURL").toBool(); } else { if (m_NexusID > 0) { // the mod id is valid, disable the custom url @@ -196,13 +196,13 @@ void ModInfoRegular::readMeta() } m_LastNexusQuery = QDateTime::fromString( - metaFile.value("lastnexusquery", "").toString(), Qt::ISODate); + metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); m_LastNexusUpdate = QDateTime::fromString( - metaFile.value("lastnexusupdate", "").toString(), Qt::ISODate); + metaFile.value("lastNexusUpdate", "").toString(), Qt::ISODate); m_NexusLastModified = QDateTime::fromString( - metaFile.value("nexuslastmodified", QDateTime::currentDateTimeUtc()).toString(), + metaFile.value("nexusLastModified", QDateTime::currentDateTimeUtc()).toString(), Qt::ISODate); - m_NexusCategory = metaFile.value("nexuscategory", 0).toInt(); + m_NexusCategory = metaFile.value("nexusCategory", 0).toInt(); m_Color = metaFile.value("color", QColor()).value<QColor>(); m_TrackedState = metaFile.value("tracked", false).toBool() ? TrackedState::TRACKED_TRUE @@ -285,26 +285,26 @@ void ModInfoRegular::saveMeta() temp.erase(m_PrimaryCategory); metaFile.setValue("category", QString("%1").arg(m_PrimaryCategory) + "," + SetJoin(temp, ",")); - metaFile.setValue("newestversion", m_NewestVersion.canonicalString()); - metaFile.setValue("ignoredversion", m_IgnoredVersion.canonicalString()); + metaFile.setValue("newestVersion", m_NewestVersion.canonicalString()); + metaFile.setValue("ignoredVersion", m_IgnoredVersion.canonicalString()); metaFile.setValue("version", m_Version.canonicalString()); - metaFile.setValue("installationfile", storeMetaPath(m_InstallationFile)); + metaFile.setValue("installationFile", storeMetaPath(m_InstallationFile)); metaFile.setValue("repository", m_Repository); - metaFile.setValue("gamename", m_GameName); + metaFile.setValue("gameName", m_GameName); metaFile.setValue("modid", m_NexusID); metaFile.setValue("comments", m_Comments); metaFile.setValue("notes", m_Notes); - metaFile.setValue("nexusdescription", m_NexusDescription); + metaFile.setValue("nexusDescription", m_NexusDescription); metaFile.setValue("url", m_CustomURL); - metaFile.setValue("hascustomurl", m_HasCustomURL); - metaFile.setValue("nexusfilestatus", m_NexusFileStatus); - metaFile.setValue("lastnexusquery", m_LastNexusQuery.toString(Qt::ISODate)); - metaFile.setValue("lastnexusupdate", m_LastNexusUpdate.toString(Qt::ISODate)); - metaFile.setValue("nexuslastmodified", m_NexusLastModified.toString(Qt::ISODate)); - metaFile.setValue("nexuscategory", m_NexusCategory); + metaFile.setValue("hasCustomURL", m_HasCustomURL); + metaFile.setValue("nexusFileStatus", m_NexusFileStatus); + metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); + metaFile.setValue("lastNexusUpdate", m_LastNexusUpdate.toString(Qt::ISODate)); + metaFile.setValue("nexusLastModified", m_NexusLastModified.toString(Qt::ISODate)); + metaFile.setValue("nexusCategory", m_NexusCategory); metaFile.setValue("author", m_Author); metaFile.setValue("uploader", m_Uploader); - metaFile.setValue("uploaderurl", m_UploaderUrl); + metaFile.setValue("uploaderUrl", m_UploaderUrl); metaFile.setValue("converted", m_Converted); metaFile.setValue("validated", m_Validated); metaFile.setValue("color", m_Color); diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index bc28969..8b3171e 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -964,9 +964,9 @@ MOBase::IModInterface* OrganizerCore::createMod(GuessedValue<QString>& name) if (!result.merged()) { settingsFile.setValue("modid", 0); settingsFile.setValue("version", ""); - settingsFile.setValue("newestversion", ""); + settingsFile.setValue("newestVersion", ""); settingsFile.setValue("category", 0); - settingsFile.setValue("installationfile", ""); + settingsFile.setValue("installationFile", ""); settingsFile.remove("installedFiles"); settingsFile.beginWriteArray("installedFiles", 0); diff --git a/src/tests/test_metainiutils.cpp b/src/tests/test_metainiutils.cpp index aef5998..faec068 100644 --- a/src/tests/test_metainiutils.cpp +++ b/src/tests/test_metainiutils.cpp @@ -40,96 +40,99 @@ TEST(MetaIniUtils, NoOpOnMissingFile) EXPECT_FALSE(MetaIniUtils::normalizeMetaIniCase(path)); } -// Sanity: file with only lowercase keys is left untouched. -TEST(MetaIniUtils, NoOpOnAlreadyLowercase) +// Sanity: file with only canonical-case keys is left untouched. +TEST(MetaIniUtils, NoOpOnAlreadyCanonical) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const auto* contents = "[General]\n" "author=Aether\n" - "category=\"11,\"\n"; + "category=\"11,\"\n" + "gameName=Skyrim\n" + "installationFile=Foo.7z\n"; const QString path = writeIni(dir, contents); EXPECT_FALSE(MetaIniUtils::normalizeMetaIniCase(path)); EXPECT_EQ(QByteArray(contents), readAll(path)); } -// The reported bug: pre-existing CamelCase keys + lowercase setValue() leaves -// duplicates on Linux/Qt6. Normalize must fold case-only duplicates. -TEST(MetaIniUtils, DedupesCaseOnlyDuplicates) +// The reported bug: pre-existing CamelCase keys + a lowercased duplicate +// (left over from the 5d1fb29 era) coexist on Linux/Qt6. Normalize must fold +// them back to the upstream MO2 case. +TEST(MetaIniUtils, FoldsLowercaseDuplicatesToUpstreamCase) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); - // Mirrors what ModInfoRegular::saveMeta() leaves behind when meta.ini - // already had CamelCase keys: both casings coexist, and the lowercase - // copy may even be empty because readMeta() couldn't see the original. + // Mirrors the dirty meta.ini in the bug report: CamelCase keys with + // empty/default values from the broken saveMeta(), and lowercase keys + // carrying the real preserved values. const auto* contents = "[General]\n" - "Author=Aether\n" - "Category=11\n" - "Color=@Variant(\\0\\0\\0\\x43\\x1\\xff\\xff\\x43\\x43\\xff\\xff\\xff\\xff\\0\\0)\n" - "Comments=keep me\n" - "Converted=false\n" - "author=\n" - "category=\"11,\"\n" - "comments=\n" - "converted=true\n"; + "gameName=SkyrimSE\n" + "gamename=Skyrim\n" + "installationFile=\n" + "installationfile=Sweet Mother HD-4947-2-0.7z\n" + "newestVersion=\n" + "newestversion=2.0.0.0\n" + "lastNexusQuery=2026-05-01T17:28:22Z\n" + "lastnexusquery=2026-05-01T17:27:42Z\n"; const QString path = writeIni(dir, contents); EXPECT_TRUE(MetaIniUtils::normalizeMetaIniCase(path)); - // Open via QSettings and verify exactly one canonical lowercase key per - // logical setting. + // Open via QSettings using the upstream CamelCase keys. QSettings s(path, QSettings::IniFormat); - EXPECT_EQ(QStringList({"author", "category", "color", "comments", "converted"}), - s.allKeys()); + // Each logical setting collapses to a single canonical-case key. + QStringList keys = s.allKeys(); + std::sort(keys.begin(), keys.end()); + EXPECT_EQ(QStringList({"gameName", "installationFile", "lastNexusQuery", + "newestVersion"}), + keys); - // Empty lowercase duplicate should NOT clobber the non-empty CamelCase - // value — `Comments=keep me` survives over `comments=`. - EXPECT_EQ("Aether", s.value("author").toString()); - EXPECT_EQ("keep me", s.value("comments").toString()); + // Empty CamelCase line dropped in favor of the non-empty lowercase value. + EXPECT_EQ("Sweet Mother HD-4947-2-0.7z", + s.value("installationFile").toString()); + EXPECT_EQ("2.0.0.0", s.value("newestVersion").toString()); - // For non-empty duplicates the latest write wins, mirroring last-writer - // semantics so QSettings doesn't reintroduce stale data. - EXPECT_EQ("11,", s.value("category").toString()); - EXPECT_TRUE(s.value("converted").toBool()); + // Both halves were non-empty for gameName and lastNexusQuery — last writer + // wins, mirroring saveMeta()'s order-of-writes. + EXPECT_EQ("Skyrim", s.value("gameName").toString()); + EXPECT_EQ("2026-05-01T17:27:42Z", s.value("lastNexusQuery").toString()); - // Color value (containing escaped binary) round-trips intact. const QByteArray after = readAll(path); - EXPECT_TRUE(after.contains( - "color=@Variant(\\0\\0\\0\\x43\\x1\\xff\\xff\\x43\\x43\\xff\\xff\\xff\\xff\\0\\0)")) - << after.toStdString(); - EXPECT_FALSE(after.contains("Author=")); - EXPECT_FALSE(after.contains("Color=")); + EXPECT_FALSE(after.contains("gamename=")); + EXPECT_FALSE(after.contains("installationfile=")); + EXPECT_FALSE(after.contains("newestversion=")); + EXPECT_FALSE(after.contains("lastnexusquery=")); } // QSettings IniFormat supports multi-line values via trailing-`\` line -// continuation. Continuation lines must travel with their key when the key is -// renamed to lowercase, otherwise the value gets corrupted. +// continuation. Continuation lines must travel with their key when the key +// is canonicalized, otherwise the value gets corrupted. TEST(MetaIniUtils, PreservesLineContinuations) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const auto* contents = "[General]\n" - "NotesField=line one\\\n" + "nexusdescription=line one\\\n" " continued line two\n" - "Author=Aether\n"; + "author=Aether\n"; const QString path = writeIni(dir, contents); EXPECT_TRUE(MetaIniUtils::normalizeMetaIniCase(path)); const QByteArray after = readAll(path); - EXPECT_TRUE(after.contains("notesfield=line one\\\n")); + EXPECT_TRUE(after.contains("nexusDescription=line one\\\n")); EXPECT_TRUE(after.contains(" continued line two")); - EXPECT_FALSE(after.contains("NotesField=")); + EXPECT_FALSE(after.contains("nexusdescription=")); } // Plugin settings live in nested `[Plugins\<name>]` sections. The dedupe -// logic must scope per-section — `Foo=` under [General] must not collide -// with `foo=` under [Plugins\Whatever]. +// logic must scope per-section — a key under [General] must not collide +// with the same lowercase key under [Plugins\Whatever]. TEST(MetaIniUtils, DedupesPerSection) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const auto* contents = "[General]\n" - "Foo=top\n" + "gameName=Skyrim\n" "[Plugins\\Sample]\n" "Foo=plugin\n" "foo=plugin-dup\n"; @@ -137,54 +140,41 @@ TEST(MetaIniUtils, DedupesPerSection) EXPECT_TRUE(MetaIniUtils::normalizeMetaIniCase(path)); QSettings s(path, QSettings::IniFormat); - EXPECT_EQ("top", s.value("foo").toString()); + EXPECT_EQ("Skyrim", s.value("gameName").toString()); s.beginGroup("Plugins/Sample"); - EXPECT_EQ("plugin-dup", s.value("foo").toString()); + // Unknown plugin key keeps its first-seen case, last-writer wins on value. + EXPECT_EQ("plugin-dup", s.value("Foo").toString()); s.endGroup(); } -// End-to-end: reproduce the exact symptom from the bug report — install -// path opens QSettings, writes lowercase keys; without normalization the -// resulting file ends up with CamelCase + lowercase duplicates. -TEST(MetaIniUtils, FixesQSettingsDuplicationOnLinux) +// End-to-end: an upstream-style saveMeta sequence over a normalized file +// updates in place — no second case-mismatched key appears. +TEST(MetaIniUtils, UpstreamSaveMetaProducesNoDupes) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); - // Pre-existing meta.ini in CamelCase (e.g. shipped inside the mod - // archive, or migrated from MO2 Windows). + // Dirty file from the 5d1fb29 era — CamelCase + lowercase coexist. const auto* preexisting = "[General]\n" - "Author=Aether\n" - "Category=11\n" - "Comments=keep me\n"; + "gameName=SkyrimSE\n" + "gamename=Skyrim\n" + "installationFile=\n" + "installationfile=Foo.7z\n"; const QString path = writeIni(dir, preexisting); - // Without normalization, lowercase setValue adds NEW keys alongside the - // CamelCase ones — confirm the bug exists in the bare QSettings flow. - { - QSettings s(path, QSettings::IniFormat); - s.setValue("author", "Aether"); - s.setValue("category", "11,"); - s.setValue("comments", ""); - } - const QByteArray dirty = readAll(path); - EXPECT_TRUE(dirty.contains("Author=Aether")); - EXPECT_TRUE(dirty.contains("author=Aether")); - - // Normalize and re-run the same install-style writes. After this the - // file must have exactly one casing per key. + // Normalize folds lowercase dupes back into the upstream CamelCase keys. EXPECT_TRUE(MetaIniUtils::normalizeMetaIniCase(path)); + + // Upstream-style saveMeta: CamelCase setValue on every key. { QSettings s(path, QSettings::IniFormat); - s.setValue("author", "Aether"); - s.setValue("category", "11,"); + s.setValue("gameName", s.value("gameName")); + s.setValue("installationFile", s.value("installationFile")); } const QByteArray clean = readAll(path); - EXPECT_FALSE(clean.contains("Author=")); - EXPECT_FALSE(clean.contains("Category=")); - EXPECT_FALSE(clean.contains("Comments=")); - EXPECT_TRUE(clean.contains("author=Aether")); - EXPECT_TRUE(clean.contains("category=\"11,\"")); - // The non-empty original Comments value survived the empty duplicate. - EXPECT_TRUE(clean.contains("comments=keep me")); + EXPECT_TRUE(clean.contains("gameName=Skyrim")); + EXPECT_TRUE(clean.contains("installationFile=Foo.7z")); + // No lowercase dupes resurrected. + EXPECT_FALSE(clean.contains("gamename=")); + EXPECT_FALSE(clean.contains("installationfile=")); } |
