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 }