190 lines
9.4 KiB
C++
190 lines
9.4 KiB
C++
/* Текстовые INI-обновления без перезаписи исходного файла до успешного commit. */
|
|
#include "config_updater.h"
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QSaveFile>
|
|
|
|
extern "C" {
|
|
#include "../../../src/config_ini.h"
|
|
}
|
|
|
|
/* Тип секции регистронезависим, имя экземпляра сохраняет регистр, как в ядре. */
|
|
static QString normalized_section(const QString& section) {
|
|
const int colon = section.indexOf(':');
|
|
if (colon >= 0) return section.left(colon).trimmed().toLower() + ':' + section.mid(colon + 1).trimmed();
|
|
const QString name = section.trimmed().toLower();
|
|
return name == "chat" ? QString("chatserver") : name;
|
|
}
|
|
|
|
static QString section_name(const QString& line) {
|
|
const QString text = line.trimmed();
|
|
return text.startsWith('[') && text.endsWith(']') ? normalized_section(text.mid(1, text.size() - 2)) : QString();
|
|
}
|
|
|
|
static bool single_line(const QString& value) {
|
|
return !value.contains('\n') && !value.contains('\r') && !value.contains(QChar(0));
|
|
}
|
|
|
|
/* Отказ до изменения lines: аргументы не могут создавать дополнительные строки/секции. */
|
|
bool config_update_in_lines(QStringList& lines, const QString& section,
|
|
const QString& key, const QString& value, bool replace) {
|
|
const bool sectionValid = !section.trimmed().isEmpty() && section == section.trimmed() && single_line(section) &&
|
|
!section.contains('[') && !section.contains(']');
|
|
const bool keyValid = !key.trimmed().isEmpty() && key == key.trimmed() && single_line(key) && !key.contains('=') &&
|
|
!key.contains('[') && !key.contains(']') && !key.startsWith('#') && !key.startsWith(';');
|
|
const bool valueValid = single_line(value);
|
|
if (!sectionValid || !keyValid || !valueValid) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config update rejected section_valid=%d key_valid=%d value_single_line=%d",
|
|
sectionValid, keyValid, valueValid);
|
|
return false;
|
|
}
|
|
const QString wanted = normalized_section(section);
|
|
const bool controlAlias = wanted == "control" && (key == "control_ip" || key == "control_port" || key == "control_allow");
|
|
const QString option = controlAlias ? key.mid(8) : key;
|
|
const QString replacement = key + '=' + value;
|
|
int lastSection = -1, lastKey = -1, replaced = 0;
|
|
bool matching = false;
|
|
for (int i = 0; i < lines.size(); ++i) {
|
|
const QString name = section_name(lines[i]);
|
|
if (!name.isEmpty()) {
|
|
matching = name == wanted;
|
|
if (matching) lastSection = i;
|
|
continue;
|
|
}
|
|
if (!matching) continue;
|
|
const int equal = lines[i].indexOf('=');
|
|
if (equal <= 0) continue;
|
|
QString candidate = lines[i].left(equal).trimmed();
|
|
if (wanted == "control" && (candidate == "control_ip" || candidate == "control_port" || candidate == "control_allow"))
|
|
candidate = candidate.mid(8);
|
|
if (candidate != option) continue;
|
|
lastKey = i;
|
|
if (replace) { lines[i] = replacement; ++replaced; }
|
|
}
|
|
if (!replaced) {
|
|
if (lastSection < 0) {
|
|
if (!lines.isEmpty() && !lines.last().trimmed().isEmpty()) lines.append("");
|
|
lines.append('[' + section + ']');
|
|
lines.append(replacement);
|
|
} else {
|
|
lines.insert(lastKey >= 0 ? lastKey + 1 : lastSection + 1, replacement);
|
|
}
|
|
}
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "GUI config prepared section=%s key=%s mode=%s replaced=%d",
|
|
qPrintable(section), qPrintable(key), replace ? "replace" : "append", replaced);
|
|
return true;
|
|
}
|
|
|
|
/* ENOENT допускает создание; ошибки чтения и непредставимый UTF-8 запрещают перезапись. */
|
|
bool config_read_lines(const QString& path, QStringList& lines, bool allowMissing) {
|
|
QFile input(path);
|
|
const QFileInfo info(path);
|
|
if (allowMissing && !info.exists() && !info.isSymLink()) { lines.clear(); return true; }
|
|
if (!input.open(QIODevice::ReadOnly)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config read open failed path=%s: %s", qPrintable(path), qPrintable(input.errorString()));
|
|
return false;
|
|
}
|
|
const QByteArray data = input.readAll();
|
|
if (input.error() != QFileDevice::NoError) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config read failed path=%s: %s", qPrintable(path), qPrintable(input.errorString()));
|
|
return false;
|
|
}
|
|
const QString text = QString::fromUtf8(data);
|
|
if (text.toUtf8() != data) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config rejected non-UTF-8 text path=%s", qPrintable(path));
|
|
return false;
|
|
}
|
|
if (config_ini_validate(data.constData(), size_t(data.size()), qPrintable(path))) return false;
|
|
QStringList result;
|
|
if (!text.isEmpty()) {
|
|
result = text.split('\n');
|
|
if (result.last().isEmpty()) result.removeLast();
|
|
for (QString& line : result) if (line.endsWith('\r')) line.chop(1);
|
|
}
|
|
lines = result;
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "GUI config read path=%s bytes=%lld lines=%lld",
|
|
qPrintable(path), (long long)data.size(), (long long)lines.size());
|
|
return true;
|
|
}
|
|
|
|
/* QSaveFile владеет соседним temp и удаляет его при ошибке; прямой fallback запрещён. */
|
|
bool config_write_lines(const QString& path, const QStringList& lines) {
|
|
QByteArray data = lines.join('\n').toUtf8();
|
|
if (!data.isEmpty() && !data.endsWith('\n')) data.append('\n');
|
|
if (config_ini_validate(data.constData(), size_t(data.size()), qPrintable(path))) return false;
|
|
QSaveFile output(path);
|
|
output.setDirectWriteFallback(false);
|
|
const char *phase = "open";
|
|
bool saved = output.open(QIODevice::WriteOnly);
|
|
if (saved) { phase = "write"; saved = output.write(data) == data.size(); }
|
|
if (saved) { phase = "commit"; saved = output.commit(); }
|
|
if (!saved) {
|
|
const int error = errno;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config save failed path=%s phase=%s bytes=%lld: %s; errno=%d (%s)",
|
|
qPrintable(path), phase, (long long)data.size(), qPrintable(output.errorString()), error, strerror(error));
|
|
return false;
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "GUI config saved path=%s bytes=%lld", qPrintable(path), (long long)data.size());
|
|
return true;
|
|
}
|
|
|
|
bool config_update_option(const QString& filePath, const QString& section,
|
|
const QString& param, const QString& value, int flags) {
|
|
if (flags != CONFIG_UPDATE_REPLACE && flags != CONFIG_UPDATE_APPEND) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config update rejected flags=%d path=%s", flags, qPrintable(filePath));
|
|
return false;
|
|
}
|
|
QStringList lines;
|
|
if (!config_read_lines(filePath, lines, true) ||
|
|
!config_update_in_lines(lines, section, param, value, flags == CONFIG_UPDATE_REPLACE)) return false;
|
|
return config_write_lines(filePath, lines);
|
|
}
|
|
|
|
bool config_rewrite_servers(const QString& filePath, const QList<ConfigServer>& servers) {
|
|
QStringList lines;
|
|
if (!config_read_lines(filePath, lines, true)) return false;
|
|
|
|
QStringList block;
|
|
int index = 0;
|
|
for (const ConfigServer& server : servers) {
|
|
const struct { const char *field; bool valid; } checks[] = {
|
|
{"name", !server.name.trimmed().isEmpty() && server.name == server.name.trimmed() && !server.name.contains(':') &&
|
|
!server.name.contains('[') && !server.name.contains(']') && single_line(server.name)},
|
|
{"addr", single_line(server.addr)}, {"transport", single_line(server.transport)},
|
|
{"type", single_line(server.type)}, {"netif", single_line(server.netif)}
|
|
};
|
|
for (const auto& check : checks) {
|
|
if (check.valid) continue;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI config rejected malformed server index=%d field=%s path=%s",
|
|
index, check.field, qPrintable(filePath));
|
|
return false;
|
|
}
|
|
block << ("[server:" + server.name + ']') << ("addr=" + server.addr);
|
|
block << ("transport=" + (server.transport.isEmpty() ? QString("udp") : server.transport));
|
|
block << ("type=" + (server.type.isEmpty() ? QString("public") : server.type));
|
|
if (!server.netif.isEmpty()) block << ("netif=" + server.netif);
|
|
block << "";
|
|
++index;
|
|
}
|
|
|
|
// Удаляем только server-секции целиком, сохраняя прочие строки в прежнем порядке.
|
|
QStringList filtered;
|
|
bool serverSection = false;
|
|
for (const QString& line : lines) {
|
|
const QString section = section_name(line);
|
|
if (!section.isEmpty()) serverSection = section.startsWith("server:");
|
|
if (!serverSection) filtered << line;
|
|
}
|
|
int insertPos = filtered.size();
|
|
bool globalSection = false;
|
|
for (int i = 0; i < filtered.size(); ++i) {
|
|
const QString section = section_name(filtered[i]);
|
|
if (section.isEmpty()) continue;
|
|
if (globalSection) { insertPos = i; break; }
|
|
globalSection = section == "global";
|
|
}
|
|
for (int i = block.size() - 1; i >= 0; --i) filtered.insert(insertPos, block[i]);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "GUI config prepared servers path=%s count=%lld", qPrintable(filePath), (long long)servers.size());
|
|
return config_write_lines(filePath, filtered);
|
|
}
|