File indexing completed on 2024-05-12 15:34:15

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include <config-kconf.h> // CMAKE_INSTALL_FULL_LIBDIR
0009 
0010 #include <cstdlib>
0011 
0012 #include <QCoreApplication>
0013 #include <QDate>
0014 #include <QDebug>
0015 #include <QDir>
0016 #include <QFile>
0017 #include <QProcess>
0018 #include <QTemporaryFile>
0019 #include <QTextStream>
0020 #include <QUrl>
0021 
0022 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0023 #include <QTextCodec>
0024 #endif
0025 
0026 #include <kconfig.h>
0027 #include <kconfiggroup.h>
0028 
0029 #include <QCommandLineOption>
0030 #include <QCommandLineParser>
0031 #include <QStandardPaths>
0032 
0033 #include "kconf_update_debug.h"
0034 #include "kconfigutils.h"
0035 
0036 // Convenience wrapper around qCDebug to prefix the output with metadata of
0037 // the file.
0038 #define qCDebugFile(CATEGORY) qCDebug(CATEGORY) << m_currentFilename << ':' << m_lineCount << ":'" << m_line << "': "
0039 
0040 static bool caseInsensitiveCompare(const QStringView &a, const QLatin1String &b)
0041 {
0042     return a.compare(b, Qt::CaseInsensitive) == 0;
0043 }
0044 
0045 class KonfUpdate
0046 {
0047 public:
0048     KonfUpdate(QCommandLineParser *parser);
0049     ~KonfUpdate();
0050 
0051     KonfUpdate(const KonfUpdate &) = delete;
0052     KonfUpdate &operator=(const KonfUpdate &) = delete;
0053 
0054     QStringList findUpdateFiles(bool dirtyOnly);
0055 
0056     bool checkFile(const QString &filename);
0057     void checkGotFile(const QString &_file, const QString &id);
0058 
0059     bool updateFile(const QString &filename);
0060 
0061     void gotId(const QString &_id);
0062     void gotFile(const QString &_file);
0063     void gotGroup(const QString &_group);
0064     void gotRemoveGroup(const QString &_group);
0065     void gotKey(const QString &_key);
0066     void gotRemoveKey(const QString &_key);
0067     void gotAllKeys();
0068     void gotAllGroups();
0069     void gotOptions(const QString &_options);
0070     void gotScript(const QString &_script);
0071     void gotScriptArguments(const QString &_arguments);
0072     void resetOptions();
0073 
0074     void copyGroup(const KConfigBase *cfg1, const QString &group1, KConfigBase *cfg2, const QString &group2);
0075     void copyGroup(const KConfigGroup &cg1, KConfigGroup &cg2);
0076     void copyOrMoveKey(const QStringList &srcGroupPath, const QString &srcKey, const QStringList &dstGroupPath, const QString &dstKey);
0077     void copyOrMoveGroup(const QStringList &srcGroupPath, const QStringList &dstGroupPath);
0078 
0079     QStringList parseGroupString(const QString &_str);
0080 
0081 protected:
0082     /** kconf_updaterc */
0083     KConfig *m_config;
0084     QString m_currentFilename;
0085     bool m_skip;
0086     bool m_skipFile;
0087     bool m_bTestMode;
0088     bool m_bDebugOutput;
0089     QString m_id;
0090 
0091     QString m_oldFile;
0092     QString m_newFile;
0093     QString m_newFileName;
0094     KConfig *m_oldConfig1; // Config to read keys from.
0095     KConfig *m_oldConfig2; // Config to delete keys from.
0096     KConfig *m_newConfig;
0097 
0098     QStringList m_oldGroup;
0099     QStringList m_newGroup;
0100 
0101     bool m_bCopy;
0102     bool m_bOverwrite;
0103     bool m_bUseConfigInfo;
0104     QString m_arguments;
0105     QTextStream *m_textStream;
0106     QFile *m_file;
0107     QString m_line;
0108     int m_lineCount;
0109 };
0110 
0111 KonfUpdate::KonfUpdate(QCommandLineParser *parser)
0112     : m_oldConfig1(nullptr)
0113     , m_oldConfig2(nullptr)
0114     , m_newConfig(nullptr)
0115     , m_bCopy(false)
0116     , m_bOverwrite(false)
0117     , m_textStream(nullptr)
0118     , m_file(nullptr)
0119     , m_lineCount(-1)
0120 {
0121     bool updateAll = false;
0122 
0123     m_config = new KConfig(QStringLiteral("kconf_updaterc"));
0124     KConfigGroup cg(m_config, QString());
0125 
0126     QStringList updateFiles;
0127 
0128     m_bDebugOutput = parser->isSet(QStringLiteral("debug"));
0129     if (m_bDebugOutput) {
0130         // The only way to enable debug reliably is through a filter rule.
0131         // The category itself is const, so we can't just go around changing
0132         // its mode. This can however be overridden by the environment, so
0133         // we'll want to have a fallback warning if debug is not enabled
0134         // after setting the filter.
0135         QLoggingCategory::setFilterRules(QLatin1String("%1.debug=true").arg(QLatin1String{KCONF_UPDATE_LOG().categoryName()}));
0136         qDebug() << "Automatically enabled the debug logging category" << KCONF_UPDATE_LOG().categoryName();
0137         if (!KCONF_UPDATE_LOG().isDebugEnabled()) {
0138             qWarning("The debug logging category %s needs to be enabled manually to get debug output", KCONF_UPDATE_LOG().categoryName());
0139         }
0140     }
0141 
0142     m_bTestMode = parser->isSet(QStringLiteral("testmode"));
0143     if (m_bTestMode) {
0144         QStandardPaths::setTestModeEnabled(true);
0145     }
0146 
0147     m_bUseConfigInfo = false;
0148     if (parser->isSet(QStringLiteral("check"))) {
0149         m_bUseConfigInfo = true;
0150         const QString file =
0151             QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String{"kconf_update/"} + parser->value(QStringLiteral("check")));
0152         if (file.isEmpty()) {
0153             qWarning("File '%s' not found.", parser->value(QStringLiteral("check")).toLocal8Bit().data());
0154             qCDebug(KCONF_UPDATE_LOG) << "File" << parser->value(QStringLiteral("check")) << "passed on command line not found";
0155             return;
0156         }
0157         updateFiles.append(file);
0158     } else if (!parser->positionalArguments().isEmpty()) {
0159         updateFiles += parser->positionalArguments();
0160     } else if (m_bTestMode) {
0161         qWarning("Test mode enabled, but no files given.");
0162         return;
0163     } else {
0164         if (cg.readEntry("autoUpdateDisabled", false)) {
0165             return;
0166         }
0167         updateFiles = findUpdateFiles(true);
0168         updateAll = true;
0169     }
0170 
0171     for (const QString &file : std::as_const(updateFiles)) {
0172         updateFile(file);
0173     }
0174 
0175     if (updateAll && !cg.readEntry("updateInfoAdded", false)) {
0176         cg.writeEntry("updateInfoAdded", true);
0177         updateFiles = findUpdateFiles(false);
0178 
0179         for (const auto &file : std::as_const(updateFiles)) {
0180             checkFile(file);
0181         }
0182         updateFiles.clear();
0183     }
0184 }
0185 
0186 KonfUpdate::~KonfUpdate()
0187 {
0188     delete m_config;
0189     delete m_file;
0190     delete m_textStream;
0191 }
0192 
0193 QStringList KonfUpdate::findUpdateFiles(bool dirtyOnly)
0194 {
0195     QStringList result;
0196 
0197     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kconf_update"), QStandardPaths::LocateDirectory);
0198     for (const QString &d : dirs) {
0199         const QDir dir(d);
0200 
0201         const QStringList fileNames = dir.entryList(QStringList(QStringLiteral("*.upd")));
0202         for (const QString &fileName : fileNames) {
0203             const QString file = dir.filePath(fileName);
0204             QFileInfo info(file);
0205 
0206             KConfigGroup cg(m_config, fileName);
0207             const qint64 ctime = cg.readEntry("ctime", 0);
0208             const qint64 mtime = cg.readEntry("mtime", 0);
0209             if (!dirtyOnly //
0210                 || (ctime != 0 && ctime != info.birthTime().toSecsSinceEpoch()) //
0211                 || mtime != info.lastModified().toSecsSinceEpoch()) {
0212                 result.append(file);
0213             }
0214         }
0215     }
0216     return result;
0217 }
0218 
0219 bool KonfUpdate::checkFile(const QString &filename)
0220 {
0221     m_currentFilename = filename;
0222     const int i = m_currentFilename.lastIndexOf(QLatin1Char{'/'});
0223     if (i != -1) {
0224         m_currentFilename = m_currentFilename.mid(i + 1);
0225     }
0226     m_skip = true;
0227     QFile file(filename);
0228     if (!file.open(QIODevice::ReadOnly)) {
0229         return false;
0230     }
0231 
0232     QTextStream ts(&file);
0233 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0234     ts.setCodec(QTextCodec::codecForName("ISO-8859-1"));
0235 #else
0236     ts.setEncoding(QStringConverter::Encoding::Latin1);
0237 #endif
0238     int lineCount = 0;
0239     resetOptions();
0240     QString id;
0241     bool foundVersion = false;
0242     while (!ts.atEnd()) {
0243         const QString line = ts.readLine().trimmed();
0244         if (line.startsWith(QLatin1String("Version=5"))) {
0245             foundVersion = true;
0246         }
0247         ++lineCount;
0248         if (line.isEmpty() || (line[0] == QLatin1Char{'#'})) {
0249             continue;
0250         }
0251         if (line.startsWith(QLatin1String("Id="))) {
0252             if (!foundVersion) {
0253                 qCDebug(KCONF_UPDATE_LOG, "Missing 'Version=5', file '%s' will be skipped.", qUtf8Printable(filename));
0254                 return true;
0255             }
0256             id = m_currentFilename + QLatin1Char{':'} + line.mid(3);
0257         } else if (line.startsWith(QLatin1String("File="))) {
0258             checkGotFile(line.mid(5), id);
0259         }
0260     }
0261 
0262     return true;
0263 }
0264 
0265 void KonfUpdate::checkGotFile(const QString &_file, const QString &id)
0266 {
0267     QString file;
0268     const int i = _file.indexOf(QLatin1Char{','});
0269     if (i == -1) {
0270         file = _file.trimmed();
0271     } else {
0272         file = _file.mid(i + 1).trimmed();
0273     }
0274 
0275     //   qDebug("File %s, id %s", file.toLatin1().constData(), id.toLatin1().constData());
0276 
0277     KConfig cfg(file, KConfig::SimpleConfig);
0278     KConfigGroup cg = cfg.group("$Version");
0279     QStringList ids = cg.readEntry("update_info", QStringList());
0280     if (ids.contains(id)) {
0281         return;
0282     }
0283     ids.append(id);
0284     cg.writeEntry("update_info", ids);
0285 }
0286 
0287 /**
0288  * Syntax:
0289  * # Comment
0290  * Id=id
0291  * File=oldfile[,newfile]
0292  * AllGroups
0293  * Group=oldgroup[,newgroup]
0294  * RemoveGroup=oldgroup
0295  * Options=[copy,][overwrite,]
0296  * Key=oldkey[,newkey]
0297  * RemoveKey=ldkey
0298  * AllKeys
0299  * Keys= [Options](AllKeys|(Key|RemoveKey)*)
0300  * ScriptArguments=arguments
0301  * Script=scriptfile[,interpreter]
0302  *
0303  * Sequence:
0304  * (Id,(File(Group,Keys)*)*)*
0305  **/
0306 bool KonfUpdate::updateFile(const QString &filename)
0307 {
0308     m_currentFilename = filename;
0309     const int i = m_currentFilename.lastIndexOf(QLatin1Char{'/'});
0310     if (i != -1) {
0311         m_currentFilename = m_currentFilename.mid(i + 1);
0312     }
0313     m_skip = true;
0314     QFile file(filename);
0315     if (!file.open(QIODevice::ReadOnly)) {
0316         qWarning("Could not open update-file '%s'.", qUtf8Printable(filename));
0317         return false;
0318     }
0319 
0320     qCDebug(KCONF_UPDATE_LOG) << "Checking update-file" << filename << "for new updates";
0321 
0322     QTextStream ts(&file);
0323 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0324     ts.setCodec(QTextCodec::codecForName("ISO-8859-1"));
0325 #else
0326     ts.setEncoding(QStringConverter::Encoding::Latin1);
0327 #endif
0328     m_lineCount = 0;
0329     resetOptions();
0330     bool foundVersion = false;
0331     while (!ts.atEnd()) {
0332         m_line = ts.readLine().trimmed();
0333         if (m_line.startsWith(QLatin1String("Version=5"))) {
0334             foundVersion = true;
0335         }
0336         ++m_lineCount;
0337         if (m_line.isEmpty() || (m_line[0] == QLatin1Char('#'))) {
0338             continue;
0339         }
0340         if (m_line.startsWith(QLatin1String("Id="))) {
0341             if (!foundVersion) {
0342                 qCDebug(KCONF_UPDATE_LOG, "Missing 'Version=5', file '%s' will be skipped.", qUtf8Printable(filename));
0343                 break;
0344             }
0345             gotId(m_line.mid(3));
0346         } else if (m_skip) {
0347             continue;
0348         } else if (m_line.startsWith(QLatin1String("Options="))) {
0349             gotOptions(m_line.mid(8));
0350         } else if (m_line.startsWith(QLatin1String("File="))) {
0351             gotFile(m_line.mid(5));
0352         } else if (m_skipFile) {
0353             continue;
0354         } else if (m_line.startsWith(QLatin1String("Group="))) {
0355             gotGroup(m_line.mid(6));
0356         } else if (m_line.startsWith(QLatin1String("RemoveGroup="))) {
0357             gotRemoveGroup(m_line.mid(12));
0358             resetOptions();
0359         } else if (m_line.startsWith(QLatin1String("Script="))) {
0360             gotScript(m_line.mid(7));
0361             resetOptions();
0362         } else if (m_line.startsWith(QLatin1String("ScriptArguments="))) {
0363             gotScriptArguments(m_line.mid(16));
0364         } else if (m_line.startsWith(QLatin1String("Key="))) {
0365             gotKey(m_line.mid(4));
0366             resetOptions();
0367         } else if (m_line.startsWith(QLatin1String("RemoveKey="))) {
0368             gotRemoveKey(m_line.mid(10));
0369             resetOptions();
0370         } else if (m_line == QLatin1String("AllKeys")) {
0371             gotAllKeys();
0372             resetOptions();
0373         } else if (m_line == QLatin1String("AllGroups")) {
0374             gotAllGroups();
0375             resetOptions();
0376         } else {
0377             qCDebugFile(KCONF_UPDATE_LOG) << "Parse error";
0378         }
0379     }
0380     // Flush.
0381     gotId(QString());
0382 
0383     // Remember that this file was updated:
0384     if (!m_bTestMode) {
0385         QFileInfo info(filename);
0386         KConfigGroup cg(m_config, m_currentFilename);
0387         if (info.birthTime().isValid()) {
0388             cg.writeEntry("ctime", info.birthTime().toSecsSinceEpoch());
0389         }
0390         cg.writeEntry("mtime", info.lastModified().toSecsSinceEpoch());
0391         cg.sync();
0392     }
0393 
0394     return true;
0395 }
0396 
0397 void KonfUpdate::gotId(const QString &_id)
0398 {
0399     // Remember that the last update group has been done:
0400     if (!m_id.isEmpty() && !m_skip && !m_bTestMode) {
0401         KConfigGroup cg(m_config, m_currentFilename);
0402 
0403         QStringList ids = cg.readEntry("done", QStringList());
0404         if (!ids.contains(m_id)) {
0405             ids.append(m_id);
0406             cg.writeEntry("done", ids);
0407             cg.sync();
0408         }
0409     }
0410 
0411     // Flush pending changes
0412     gotFile(QString());
0413 
0414     if (_id.isEmpty()) {
0415         return;
0416     }
0417 
0418     // Check whether this update group needs to be done:
0419     KConfigGroup cg(m_config, m_currentFilename);
0420     QStringList ids = cg.readEntry("done", QStringList());
0421     if (ids.contains(_id) && !m_bUseConfigInfo) {
0422         // qDebug("Id '%s' was already in done-list", _id.toLatin1().constData());
0423         m_skip = true;
0424         return;
0425     }
0426     m_skip = false;
0427     m_skipFile = false;
0428     m_id = _id;
0429     if (m_bUseConfigInfo) {
0430         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Checking update" << _id;
0431     } else {
0432         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Found new update" << _id;
0433     }
0434 }
0435 
0436 void KonfUpdate::gotFile(const QString &_file)
0437 {
0438     // Reset group
0439     gotGroup(QString());
0440 
0441     if (!m_oldFile.isEmpty()) {
0442         // Close old file.
0443         delete m_oldConfig1;
0444         m_oldConfig1 = nullptr;
0445 
0446         KConfigGroup cg(m_oldConfig2, "$Version");
0447         QStringList ids = cg.readEntry("update_info", QStringList());
0448         QString cfg_id = m_currentFilename + QLatin1Char{':'} + m_id;
0449         if (!ids.contains(cfg_id) && !m_skip) {
0450             ids.append(cfg_id);
0451             cg.writeEntry("update_info", ids);
0452         }
0453         cg.sync();
0454         delete m_oldConfig2;
0455         m_oldConfig2 = nullptr;
0456 
0457         QString file = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + m_oldFile;
0458         QFileInfo info(file);
0459         if (info.exists() && info.size() == 0) {
0460             // Delete empty file.
0461             QFile::remove(file);
0462         }
0463 
0464         m_oldFile.clear();
0465     }
0466     if (!m_newFile.isEmpty()) {
0467         // Close new file.
0468         KConfigGroup cg(m_newConfig, "$Version");
0469         QStringList ids = cg.readEntry("update_info", QStringList());
0470         const QString cfg_id = m_currentFilename + QLatin1Char{':'} + m_id;
0471         if (!ids.contains(cfg_id) && !m_skip) {
0472             ids.append(cfg_id);
0473             cg.writeEntry("update_info", ids);
0474         }
0475         m_newConfig->sync();
0476         delete m_newConfig;
0477         m_newConfig = nullptr;
0478 
0479         m_newFile.clear();
0480     }
0481     m_newConfig = nullptr;
0482 
0483     const int i = _file.indexOf(QLatin1Char{','});
0484     if (i == -1) {
0485         m_oldFile = _file.trimmed();
0486     } else {
0487         m_oldFile = _file.left(i).trimmed();
0488         m_newFile = _file.mid(i + 1).trimmed();
0489         if (m_oldFile == m_newFile) {
0490             m_newFile.clear();
0491         }
0492     }
0493 
0494     if (!m_oldFile.isEmpty()) {
0495         m_oldConfig2 = new KConfig(m_oldFile, KConfig::NoGlobals);
0496         const QString cfg_id = m_currentFilename + QLatin1Char{':'} + m_id;
0497         KConfigGroup cg(m_oldConfig2, "$Version");
0498         QStringList ids = cg.readEntry("update_info", QStringList());
0499         if (ids.contains(cfg_id)) {
0500             m_skip = true;
0501             m_newFile.clear();
0502             qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Skipping update" << m_id;
0503         }
0504 
0505         if (!m_newFile.isEmpty()) {
0506             m_newConfig = new KConfig(m_newFile, KConfig::NoGlobals);
0507             KConfigGroup cg(m_newConfig, "$Version");
0508             ids = cg.readEntry("update_info", QStringList());
0509             if (ids.contains(cfg_id)) {
0510                 m_skip = true;
0511                 qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Skipping update" << m_id;
0512             }
0513         } else {
0514             m_newConfig = m_oldConfig2;
0515         }
0516 
0517         m_oldConfig1 = new KConfig(m_oldFile, KConfig::NoGlobals);
0518     } else {
0519         m_newFile.clear();
0520     }
0521     m_newFileName = m_newFile;
0522     if (m_newFileName.isEmpty()) {
0523         m_newFileName = m_oldFile;
0524     }
0525 
0526     m_skipFile = false;
0527     if (!m_oldFile.isEmpty()) { // if File= is specified, it doesn't exist, is empty or contains only kconf_update's [$Version] group, skip
0528         if (m_oldConfig1 != nullptr
0529             && (m_oldConfig1->groupList().isEmpty()
0530                 || (m_oldConfig1->groupList().count() == 1 && m_oldConfig1->groupList().at(0) == QLatin1String("$Version")))) {
0531             qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": File" << m_oldFile << "does not exist or empty, skipping";
0532             m_skipFile = true;
0533         }
0534     }
0535 }
0536 
0537 QStringList KonfUpdate::parseGroupString(const QString &str)
0538 {
0539     bool ok;
0540     QString error;
0541     QStringList lst = KConfigUtils::parseGroupString(str, &ok, &error);
0542     if (!ok) {
0543         qCDebugFile(KCONF_UPDATE_LOG) << error;
0544     }
0545     return lst;
0546 }
0547 
0548 void KonfUpdate::gotGroup(const QString &_group)
0549 {
0550     QString group = _group.trimmed();
0551     if (group.isEmpty()) {
0552         m_oldGroup = m_newGroup = QStringList();
0553         return;
0554     }
0555 
0556     const QStringList tokens = group.split(QLatin1Char{','});
0557     m_oldGroup = parseGroupString(tokens.at(0));
0558     if (tokens.count() == 1) {
0559         m_newGroup = m_oldGroup;
0560     } else {
0561         m_newGroup = parseGroupString(tokens.at(1));
0562     }
0563 }
0564 
0565 void KonfUpdate::gotRemoveGroup(const QString &_group)
0566 {
0567     m_oldGroup = parseGroupString(_group);
0568 
0569     if (!m_oldConfig1) {
0570         qCDebugFile(KCONF_UPDATE_LOG) << "RemoveGroup without previous File specification";
0571         return;
0572     }
0573 
0574     KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig2, m_oldGroup);
0575     if (!cg.exists()) {
0576         return;
0577     }
0578     // Delete group.
0579     cg.deleteGroup();
0580     qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": RemoveGroup removes group" << m_oldFile << ":" << m_oldGroup;
0581 }
0582 
0583 void KonfUpdate::gotKey(const QString &_key)
0584 {
0585     QString oldKey;
0586     QString newKey;
0587     const int i = _key.indexOf(QLatin1Char{','});
0588     if (i == -1) {
0589         oldKey = _key.trimmed();
0590         newKey = oldKey;
0591     } else {
0592         oldKey = _key.left(i).trimmed();
0593         newKey = _key.mid(i + 1).trimmed();
0594     }
0595 
0596     if (oldKey.isEmpty() || newKey.isEmpty()) {
0597         qCDebugFile(KCONF_UPDATE_LOG) << "Key specifies invalid key";
0598         return;
0599     }
0600     if (!m_oldConfig1) {
0601         qCDebugFile(KCONF_UPDATE_LOG) << "Key without previous File specification";
0602         return;
0603     }
0604     copyOrMoveKey(m_oldGroup, oldKey, m_newGroup, newKey);
0605 }
0606 
0607 void KonfUpdate::copyOrMoveKey(const QStringList &srcGroupPath, const QString &srcKey, const QStringList &dstGroupPath, const QString &dstKey)
0608 {
0609     KConfigGroup dstCg = KConfigUtils::openGroup(m_newConfig, dstGroupPath);
0610     if (!m_bOverwrite && dstCg.hasKey(dstKey)) {
0611         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Skipping" << m_newFileName << ":" << dstCg.name() << ":" << dstKey << ", already exists.";
0612         return;
0613     }
0614 
0615     KConfigGroup srcCg = KConfigUtils::openGroup(m_oldConfig1, srcGroupPath);
0616     if (!srcCg.hasKey(srcKey)) {
0617         return;
0618     }
0619     QString value = srcCg.readEntry(srcKey, QString());
0620     qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Updating" << m_newFileName << ":" << dstCg.name() << ":" << dstKey << "to" << value;
0621     dstCg.writeEntry(dstKey, value);
0622 
0623     if (m_bCopy) {
0624         return; // Done.
0625     }
0626 
0627     // Delete old entry
0628     if (m_oldConfig2 == m_newConfig && srcGroupPath == dstGroupPath && srcKey == dstKey) {
0629         return; // Don't delete!
0630     }
0631     KConfigGroup srcCg2 = KConfigUtils::openGroup(m_oldConfig2, srcGroupPath);
0632     srcCg2.deleteEntry(srcKey);
0633     qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Removing" << m_oldFile << ":" << srcCg2.name() << ":" << srcKey << ", moved.";
0634 }
0635 
0636 void KonfUpdate::copyOrMoveGroup(const QStringList &srcGroupPath, const QStringList &dstGroupPath)
0637 {
0638     KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig1, srcGroupPath);
0639 
0640     // Keys
0641     const QStringList lstKeys = cg.keyList();
0642     for (const QString &key : lstKeys) {
0643         copyOrMoveKey(srcGroupPath, key, dstGroupPath, key);
0644     }
0645 
0646     // Subgroups
0647     const QStringList lstGroup = cg.groupList();
0648     for (const QString &group : lstGroup) {
0649         const QStringList groupPath(group);
0650         copyOrMoveGroup(srcGroupPath + groupPath, dstGroupPath + groupPath);
0651     }
0652 }
0653 
0654 void KonfUpdate::gotRemoveKey(const QString &_key)
0655 {
0656     QString key = _key.trimmed();
0657 
0658     if (key.isEmpty()) {
0659         qCDebugFile(KCONF_UPDATE_LOG) << "RemoveKey specifies invalid key";
0660         return;
0661     }
0662 
0663     if (!m_oldConfig1) {
0664         qCDebugFile(KCONF_UPDATE_LOG) << "Key without previous File specification";
0665         return;
0666     }
0667 
0668     KConfigGroup cg1 = KConfigUtils::openGroup(m_oldConfig1, m_oldGroup);
0669     if (!cg1.hasKey(key)) {
0670         return;
0671     }
0672     qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": RemoveKey removes" << m_oldFile << ":" << m_oldGroup << ":" << key;
0673 
0674     // Delete old entry
0675     KConfigGroup cg2 = KConfigUtils::openGroup(m_oldConfig2, m_oldGroup);
0676     cg2.deleteEntry(key);
0677     /*if (m_oldConfig2->deleteGroup(m_oldGroup, KConfig::Normal)) { // Delete group if empty.
0678        qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Removing empty group " << m_oldFile << ":" << m_oldGroup;
0679     }   (this should be automatic)*/
0680 }
0681 
0682 void KonfUpdate::gotAllKeys()
0683 {
0684     if (!m_oldConfig1) {
0685         qCDebugFile(KCONF_UPDATE_LOG) << "AllKeys without previous File specification";
0686         return;
0687     }
0688 
0689     copyOrMoveGroup(m_oldGroup, m_newGroup);
0690 }
0691 
0692 void KonfUpdate::gotAllGroups()
0693 {
0694     if (!m_oldConfig1) {
0695         qCDebugFile(KCONF_UPDATE_LOG) << "AllGroups without previous File specification";
0696         return;
0697     }
0698 
0699     const QStringList allGroups = m_oldConfig1->groupList();
0700     for (const auto &grp : allGroups) {
0701         m_oldGroup = QStringList{grp};
0702         m_newGroup = m_oldGroup;
0703         gotAllKeys();
0704     }
0705 }
0706 
0707 void KonfUpdate::gotOptions(const QString &_options)
0708 {
0709     const QStringList options = _options.split(QLatin1Char{','});
0710     for (const auto &opt : options) {
0711         const auto normalizedOpt = QStringView(opt).trimmed();
0712 
0713         if (caseInsensitiveCompare(normalizedOpt, QLatin1String("copy"))) {
0714             m_bCopy = true;
0715         } else if (caseInsensitiveCompare(normalizedOpt, QLatin1String("overwrite"))) {
0716             m_bOverwrite = true;
0717         }
0718     }
0719 }
0720 
0721 void KonfUpdate::copyGroup(const KConfigBase *cfg1, const QString &group1, KConfigBase *cfg2, const QString &group2)
0722 {
0723     KConfigGroup cg2 = cfg2->group(group2);
0724     copyGroup(cfg1->group(group1), cg2);
0725 }
0726 
0727 void KonfUpdate::copyGroup(const KConfigGroup &cg1, KConfigGroup &cg2)
0728 {
0729     // Copy keys
0730     const auto map = cg1.entryMap();
0731     for (auto it = map.cbegin(); it != map.cend(); ++it) {
0732         if (m_bOverwrite || !cg2.hasKey(it.key())) {
0733             cg2.writeEntry(it.key(), it.value());
0734         }
0735     }
0736 
0737     // Copy subgroups
0738     const QStringList lstGroup = cg1.groupList();
0739     for (const QString &group : lstGroup) {
0740         copyGroup(&cg1, group, &cg2, group);
0741     }
0742 }
0743 
0744 void KonfUpdate::gotScriptArguments(const QString &_arguments)
0745 {
0746     m_arguments = _arguments;
0747 }
0748 
0749 void KonfUpdate::gotScript(const QString &_script)
0750 {
0751     QString script;
0752     QString interpreter;
0753     const int i = _script.indexOf(QLatin1Char{','});
0754     if (i == -1) {
0755         script = _script.trimmed();
0756     } else {
0757         script = _script.left(i).trimmed();
0758         interpreter = _script.mid(i + 1).trimmed();
0759     }
0760 
0761     if (script.isEmpty()) {
0762         qCDebugFile(KCONF_UPDATE_LOG) << "Script fails to specify filename";
0763         m_skip = true;
0764         return;
0765     }
0766 
0767     QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kconf_update/") + script);
0768     if (path.isEmpty()) {
0769         if (interpreter.isEmpty()) {
0770             path = QStringLiteral("%1/kconf_update_bin/%2").arg(QStringLiteral(CMAKE_INSTALL_FULL_LIBDIR), script);
0771             if (!QFile::exists(path)) {
0772                 path = QStandardPaths::findExecutable(script);
0773             }
0774         }
0775 
0776         if (path.isEmpty()) {
0777             qCDebugFile(KCONF_UPDATE_LOG) << "Script" << script << "not found";
0778             m_skip = true;
0779             return;
0780         }
0781     }
0782 
0783     if (!m_arguments.isNull()) {
0784         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script << "with arguments" << m_arguments;
0785     } else {
0786         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script;
0787     }
0788 
0789     QStringList args;
0790     QString cmd;
0791     if (interpreter.isEmpty()) {
0792         cmd = path;
0793     } else {
0794         QString interpreterPath = QStandardPaths::findExecutable(interpreter);
0795         if (interpreterPath.isEmpty()) {
0796             qCDebugFile(KCONF_UPDATE_LOG) << "Cannot find interpreter" << interpreter;
0797             m_skip = true;
0798             return;
0799         }
0800         cmd = interpreterPath;
0801         args << path;
0802     }
0803 
0804     if (!m_arguments.isNull()) {
0805         args += m_arguments;
0806     }
0807 
0808     QTemporaryFile scriptIn;
0809     QTemporaryFile scriptOut;
0810     if (!scriptIn.open() || !scriptOut.open()) {
0811         qCDebugFile(KCONF_UPDATE_LOG) << "Could not create temporary file!";
0812         return;
0813     }
0814 
0815     int result;
0816     QProcess proc;
0817     proc.setProcessChannelMode(QProcess::SeparateChannels);
0818     proc.setStandardInputFile(scriptIn.fileName());
0819     proc.setStandardOutputFile(scriptOut.fileName());
0820     if (m_oldConfig1) {
0821         if (m_bDebugOutput) {
0822             qCDebug(KCONF_UPDATE_LOG) << "Script input stored in" << scriptIn.fileName();
0823         }
0824         KConfig cfg(scriptIn.fileName(), KConfig::SimpleConfig);
0825 
0826         if (m_oldGroup.isEmpty()) {
0827             // Write all entries to tmpFile;
0828             const QStringList grpList = m_oldConfig1->groupList();
0829             for (const auto &grp : grpList) {
0830                 copyGroup(m_oldConfig1, grp, &cfg, grp);
0831             }
0832         } else {
0833             KConfigGroup cg1 = KConfigUtils::openGroup(m_oldConfig1, m_oldGroup);
0834             KConfigGroup cg2(&cfg, QString());
0835             copyGroup(cg1, cg2);
0836         }
0837         cfg.sync();
0838     }
0839 
0840     qCDebug(KCONF_UPDATE_LOG) << "About to run" << cmd;
0841     if (m_bDebugOutput) {
0842         QFile scriptFile(path);
0843         if (scriptFile.open(QIODevice::ReadOnly)) {
0844             qCDebug(KCONF_UPDATE_LOG) << "Script contents is:\n" << scriptFile.readAll();
0845         }
0846     }
0847     proc.start(cmd, args);
0848     if (!proc.waitForFinished(60000)) {
0849         qCDebugFile(KCONF_UPDATE_LOG) << "update script did not terminate within 60 seconds:" << cmd;
0850         m_skip = true;
0851         return;
0852     }
0853     result = proc.exitCode();
0854 
0855     // Copy script stderr to log file
0856     {
0857         QTextStream ts(proc.readAllStandardError());
0858 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0859         ts.setCodec(QTextCodec::codecForName("UTF-8"));
0860 #endif
0861         while (!ts.atEnd()) {
0862             QString line = ts.readLine();
0863             qCDebug(KCONF_UPDATE_LOG) << "[Script]" << line;
0864         }
0865     }
0866     proc.close();
0867 
0868     if (result != EXIT_SUCCESS) {
0869         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": !! An error occurred while running" << cmd;
0870         return;
0871     }
0872 
0873     qCDebug(KCONF_UPDATE_LOG) << "Successfully ran" << cmd;
0874 
0875     if (!m_oldConfig1) {
0876         return; // Nothing to merge
0877     }
0878 
0879     if (m_bDebugOutput) {
0880         qCDebug(KCONF_UPDATE_LOG) << "Script output stored in" << scriptOut.fileName();
0881         QFile output(scriptOut.fileName());
0882         if (output.open(QIODevice::ReadOnly)) {
0883             qCDebug(KCONF_UPDATE_LOG) << "Script output is:\n" << output.readAll();
0884         }
0885     }
0886 
0887     // Deleting old entries
0888     {
0889         QStringList group = m_oldGroup;
0890         QFile output(scriptOut.fileName());
0891         if (output.open(QIODevice::ReadOnly)) {
0892             QTextStream ts(&output);
0893 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0894             ts.setCodec(QTextCodec::codecForName("UTF-8"));
0895 #endif
0896             while (!ts.atEnd()) {
0897                 const QString line = ts.readLine();
0898                 if (line.startsWith(QLatin1Char{'['})) {
0899                     group = parseGroupString(line);
0900                 } else if (line.startsWith(QLatin1String("# DELETE "))) {
0901                     QString key = line.mid(9);
0902                     if (key.startsWith(QLatin1Char{'['})) {
0903                         const int idx = key.lastIndexOf(QLatin1Char{']'}) + 1;
0904                         if (idx > 0) {
0905                             group = parseGroupString(key.left(idx));
0906                             key = key.mid(idx);
0907                         }
0908                     }
0909                     KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig2, group);
0910                     cg.deleteEntry(key);
0911                     qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Script removes" << m_oldFile << ":" << group << ":" << key;
0912                     /*if (m_oldConfig2->deleteGroup(group, KConfig::Normal)) { // Delete group if empty.
0913                        qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Removing empty group " << m_oldFile << ":" << group;
0914                     } (this should be automatic)*/
0915                 } else if (line.startsWith(QLatin1String("# DELETEGROUP"))) {
0916                     const QString str = line.mid(13).trimmed();
0917                     if (!str.isEmpty()) {
0918                         group = parseGroupString(str);
0919                     }
0920                     KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig2, group);
0921                     cg.deleteGroup();
0922                     qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Script removes group" << m_oldFile << ":" << group;
0923                 }
0924             }
0925         }
0926     }
0927 
0928     // Merging in new entries.
0929     KConfig scriptOutConfig(scriptOut.fileName(), KConfig::NoGlobals);
0930     if (m_newGroup.isEmpty()) {
0931         // Copy "default" keys as members of "default" keys
0932         copyGroup(&scriptOutConfig, QString(), m_newConfig, QString());
0933     } else {
0934         // Copy default keys as members of m_newGroup
0935         KConfigGroup srcCg = KConfigUtils::openGroup(&scriptOutConfig, QStringList());
0936         KConfigGroup dstCg = KConfigUtils::openGroup(m_newConfig, m_newGroup);
0937         copyGroup(srcCg, dstCg);
0938     }
0939     const QStringList lstGroup = scriptOutConfig.groupList();
0940     for (const QString &group : lstGroup) {
0941         copyGroup(&scriptOutConfig, group, m_newConfig, group);
0942     }
0943 }
0944 
0945 void KonfUpdate::resetOptions()
0946 {
0947     m_bCopy = false;
0948     m_bOverwrite = false;
0949     m_arguments.clear();
0950 }
0951 
0952 int main(int argc, char **argv)
0953 {
0954     QCoreApplication app(argc, argv);
0955     app.setApplicationVersion(QStringLiteral("1.1"));
0956 
0957     QCommandLineParser parser;
0958     parser.addVersionOption();
0959     parser.setApplicationDescription(QCoreApplication::translate("main", "KDE Tool for updating user configuration files"));
0960     parser.addHelpOption();
0961     parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("debug"), QCoreApplication::translate("main", "Keep output results from scripts")));
0962     parser.addOption(
0963         QCommandLineOption(QStringList() << QStringLiteral("testmode"),
0964                            QCoreApplication::translate("main", "For unit tests only: use test directories to stay away from the user's real files")));
0965     parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("check"),
0966                                         QCoreApplication::translate("main", "Check whether config file itself requires updating"),
0967                                         QStringLiteral("update-file")));
0968     parser.addPositionalArgument(QStringLiteral("files"),
0969                                  QCoreApplication::translate("main", "File(s) to read update instructions from"),
0970                                  QStringLiteral("[files...]"));
0971 
0972     // TODO aboutData.addAuthor(ki18n("Waldo Bastian"), KLocalizedString(), "bastian@kde.org");
0973 
0974     parser.process(app);
0975     KonfUpdate konfUpdate(&parser);
0976 
0977     return 0;
0978 }