Prod: полный аудит и стабилизация — исправлены критические баги, расширено покрытие тестами, подготовлен бандл

- FileStore: защита от выхода за пределы хранилища (isInsideVault), валидация имён (запрет скрытых, зарезервированных, управляющих символов), проверка destDir, лимит watcher, dailyNote валидация, openVault проверка isDir
- DocumentManager: лимит 10МБ, кламп автосейва 0.5-120с, защита от OOM, бинарный детект, проверка путей
- VaultIndex: исправлен парсинг тегов (заголовки, инлайн-код), case-insensitive поиск, дедупликация рёбер, защита от traversal, лимит глубины, проверка vaultExists
- MdParser: блокировка всех on* событий, javascript:/data:, esc кавычек, MIME fallback, normalizeLang расширен (c/cpp/py/js и кириллица), wiki глобальный поиск, image лимит 3МБ, защита clipboard
- GitManager: commitAll с GIT_INDEX_ADD_DEFAULT и поддержкой пустых коммитов/удалений, fileGitStatus с cleanPath и проверкой префикса, repoExists/hasCommits с cleanPath, initRepo валидация
- SettingsStore: encodeKey/encodeRel для QSettings Ini, flat хранение viewMode, sync после записи, кламп ширин панелей, миграция старых ключей
- SyncManager: счётчик busy, обработка httpStatus при network error, валидация токена/провайдера/URL, GitHub Enterprise URL, таймаут
- VaultManager: валидация путей (корень, файл vs папка, права), проверка вложенности
- WindowController: per-window FileStore/DocumentManager/VaultIndex, cleanPath, закрытие окна через QQuickWindow::closing, обработка ошибок компонента
- QML: TableEditor пустая строка, WelcomeWizard QUrl парсинг, FileTree фильтр, GraphView лимит 600 узлов, RightPanel debounce rebuild, Theme singleton, MonthCalendar и др.
- Тесты: добавлены flora_settings_test, flora_sync_test, flora_extended_test, flora_integration_test — покрытие всех модулей, 20/20 зелёных
- Сборка: обновлён Flora.app (4.6М бинарь -> 173М бандл, ad-hoc подпись, проверен offscreen запуск, версия 0.0.1 + cmark 0.29 + libgit2 1.9)
- .gitignore: добавлены Testing/, coverage, gcda/gcov, __pycache__, .tmp, test-vault и др.
This commit is contained in:
2026-09-01 19:25:07 +03:00
parent 0858b48cb3
commit 95c03040c7
41 changed files with 3868 additions and 1098 deletions
+187 -5
View File
@@ -1,12 +1,31 @@
#include "core/SettingsStore.h"
#include <QDir>
#include <QSettings>
#include <QUrl>
#include <algorithm>
namespace {
constexpr auto kGroupVaults = "vaults";
constexpr auto kKeyVaults = "vaults/vaultList";
constexpr auto kKeyCurrent = "vaults/current";
constexpr auto kKeyProviders = "sync/providers";
constexpr auto kKeyTheme = "ui/theme";
constexpr auto kKeyLeftW = "ui/leftPanelWidth";
constexpr auto kKeyRightW = "ui/rightPanelWidth";
constexpr auto kKeyViewModes = "ui/viewModes";
QString encodeKey(const QString &s) {
// Процент-кодирование чтобы избежать интерпретации "/" как разделителя групп в QSettings Ini
return QString::fromLatin1(QUrl::toPercentEncoding(s, QByteArray(), QByteArray("/")));
}
QString encodeRel(const QString &rel) {
// Для относительных путей тоже кодируем "/" как %2F чтобы ключ остался плоским
return QString::fromLatin1(QUrl::toPercentEncoding(rel));
}
QString decodeKey(const QString &s) {
return QUrl::fromPercentEncoding(s.toLatin1());
}
}
SettingsStore::SettingsStore(QObject *parent)
@@ -31,7 +50,8 @@ QString SettingsStore::currentVault() const
void SettingsStore::setCurrentVault(const QString &path)
{
QSettings s;
s.setValue(kKeyCurrent, path);
s.setValue(kKeyCurrent, QDir::cleanPath(path));
s.sync();
emit currentVaultChanged();
}
@@ -42,18 +62,26 @@ QVariantList SettingsStore::providers() const
void SettingsStore::addVault(const QString &path)
{
const QString clean = QDir::cleanPath(path);
if (clean.isEmpty())
return;
QStringList list = vaults();
if (!list.contains(path))
list.prepend(path);
// нормализуем существующие записи
for (QString &v : list) v = QDir::cleanPath(v);
list.removeAll(clean);
list.prepend(clean);
QSettings s;
s.setValue(kKeyVaults, list);
s.setValue(kKeyCurrent, path);
s.setValue(kKeyCurrent, clean);
s.sync();
emit vaultsChanged();
emit currentVaultChanged();
}
void SettingsStore::addProvider(const QString &provider, const QVariantMap &config)
{
if (provider.isEmpty())
return;
QVariantList list = m_providers;
for (auto &item : list) {
auto m = item.toMap();
@@ -74,12 +102,18 @@ void SettingsStore::addProvider(const QString &provider, const QVariantMap &conf
save:
QSettings s;
s.setValue(kKeyProviders, m_providers);
s.sync();
emit providersChanged();
}
bool SettingsStore::hasVault(const QString &path) const
{
return vaults().contains(path);
const QString clean = QDir::cleanPath(path);
const QStringList list = vaults();
for (const QString &v : list)
if (QDir::cleanPath(v) == clean)
return true;
return false;
}
void SettingsStore::clearProvider(const QString &provider)
@@ -93,5 +127,153 @@ void SettingsStore::clearProvider(const QString &provider)
m_providers = list;
QSettings s;
s.setValue(kKeyProviders, m_providers);
s.sync();
emit providersChanged();
}
QString SettingsStore::theme() const
{
QSettings s;
const QString t = s.value(kKeyTheme, QStringLiteral("light")).toString();
return (t == "dark" || t == "light") ? t : QStringLiteral("light");
}
void SettingsStore::setTheme(const QString &t)
{
const QString clean = (t == "dark") ? QStringLiteral("dark") : QStringLiteral("light");
QSettings s;
if (s.value(kKeyTheme).toString() == clean)
return;
s.setValue(kKeyTheme, clean);
s.sync();
emit themeChanged();
}
int SettingsStore::leftPanelWidth() const
{
QSettings s;
return s.value(kKeyLeftW, 220).toInt();
}
void SettingsStore::setLeftPanelWidth(int w)
{
if (w < 100) w = 100;
if (w > 800) w = 800;
QSettings s;
if (s.value(kKeyLeftW).toInt() == w)
return;
s.setValue(kKeyLeftW, w);
s.sync();
emit leftPanelWidthChanged();
}
int SettingsStore::rightPanelWidth() const
{
QSettings s;
return s.value(kKeyRightW, 220).toInt();
}
void SettingsStore::setRightPanelWidth(int w)
{
if (w < 100) w = 100;
if (w > 800) w = 800;
QSettings s;
if (s.value(kKeyRightW).toInt() == w)
return;
s.setValue(kKeyRightW, w);
s.sync();
emit rightPanelWidthChanged();
}
QString SettingsStore::viewModeForFile(const QString &vault, const QString &filePath) const
{
if (vault.isEmpty() || filePath.isEmpty())
return QStringLiteral("edit");
QSettings s;
const QVariantMap root = s.value(kKeyViewModes).toMap();
const QString cleanVault = QDir::cleanPath(vault);
const QString cleanPath = QDir::cleanPath(filePath);
QString rel = cleanPath;
if (cleanPath.startsWith(cleanVault)) {
rel = cleanPath.mid(cleanVault.length());
while (rel.startsWith("/") || rel.startsWith("\\")) rel.remove(0,1);
} else {
rel = cleanPath;
}
const QString oldKey = cleanVault + "|" + rel;
if (root.contains(oldKey)) {
const QString m = root.value(oldKey).toString();
if (m == "preview" || m == "live" || m == "edit") return m;
}
const QString encVault = encodeKey(cleanVault);
const QString encRel = encodeRel(rel);
const QString encKey = encVault + "|" + encRel;
if (root.contains(encKey)) {
const QString m = root.value(encKey).toString();
if (m == "preview" || m == "live" || m == "edit") return m;
}
// Обратная совместимость: старый вложенный формат
QVariantMap vaultMap = root.value(encVault).toMap();
if (vaultMap.isEmpty())
vaultMap = root.value(cleanVault).toMap();
QString mode = vaultMap.value(encRel).toString();
if (mode.isEmpty())
mode = vaultMap.value(rel).toString();
if (mode == "preview" || mode == "live" || mode == "edit")
return mode;
return QStringLiteral("edit");
}
void SettingsStore::setViewModeForFile(const QString &vault, const QString &filePath, const QString &mode)
{
if (vault.isEmpty() || filePath.isEmpty())
return;
QString cleanMode = mode;
if (cleanMode != "preview" && cleanMode != "live")
cleanMode = "edit";
QSettings s;
QVariantMap root = s.value(kKeyViewModes).toMap();
const QString cleanVault = QDir::cleanPath(vault);
const QString cleanPath = QDir::cleanPath(filePath);
QString rel = cleanPath;
if (cleanPath.startsWith(cleanVault)) {
rel = cleanPath.mid(cleanVault.length());
while (rel.startsWith("/") || rel.startsWith("\\")) rel.remove(0,1);
} else {
rel = cleanPath;
}
const QString encVault = encodeKey(cleanVault);
const QString encRel = encodeRel(rel);
const QString encKey = encVault + "|" + encRel;
// мигрируем старые ключи
const QString oldKey = cleanVault + "|" + rel;
if (root.contains(oldKey))
root.remove(oldKey);
// мигрируем старый вложенный формат если есть
if (root.contains(encVault) && root.value(encVault).typeId() == QMetaType::QVariantMap) {
QVariantMap vm = root.value(encVault).toMap();
if (!vm.isEmpty()) {
// переносим все вложенные записи в плоский формат
for (auto it = vm.constBegin(); it != vm.constEnd(); ++it) {
const QString innerKey = encVault + "|" + it.key();
if (!root.contains(innerKey))
root.insert(innerKey, it.value());
}
root.remove(encVault);
}
}
if (root.contains(cleanVault) && root.value(cleanVault).typeId() == QMetaType::QVariantMap) {
QVariantMap vm = root.value(cleanVault).toMap();
root.remove(cleanVault);
for (auto it = vm.constBegin(); it != vm.constEnd(); ++it) {
const QString k = encodeKey(cleanVault) + "|" + encodeRel(it.key());
if (!root.contains(k))
root.insert(k, it.value());
}
}
if (root.value(encKey).toString() == cleanMode)
return;
root.insert(encKey, cleanMode);
s.setValue(kKeyViewModes, root);
s.sync();
}