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:
+84
-17
@@ -1,13 +1,15 @@
|
||||
#include "core/VaultIndex.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
|
||||
namespace {
|
||||
|
||||
QStringList kIgnoredNames = {".git", ".flora", ".stfolder"};
|
||||
QStringList kIgnoredNames = {".git", ".flora", ".stfolder", ".DS_Store", "Thumbs.db"};
|
||||
|
||||
// Извлекает теги #tag из текста (пропуская блоки кода).
|
||||
QStringList extractTags(const QString &text)
|
||||
@@ -15,19 +17,24 @@ QStringList extractTags(const QString &text)
|
||||
QStringList tags;
|
||||
static const QRegularExpression tagRe(
|
||||
QStringLiteral("(?:^|[^\\w#])#([\\p{L}\\w][\\p{L}\\w-]*)"));
|
||||
static const QRegularExpression inlineCodeRe(QStringLiteral("`[^`]*`"));
|
||||
bool inCode = false;
|
||||
const QStringList lines = text.split(QLatin1Char('\n'));
|
||||
for (const QString &line : lines) {
|
||||
const QString t = line.trimmed();
|
||||
if (t.startsWith(QLatin1String("```"))) {
|
||||
for (const QString &origLine : lines) {
|
||||
const QString t = origLine.trimmed();
|
||||
if (t.startsWith(QLatin1String("```")) || t.startsWith(QLatin1String("~~~"))) {
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
if (inCode)
|
||||
continue;
|
||||
// Заголовок "# Заголовок" — не тег (после # идёт пробел)
|
||||
if (t.startsWith(QLatin1String("# ")) || t.startsWith(QLatin1String("## ")))
|
||||
continue;
|
||||
// Убираем инлайн-код `code #tag` — теги внутри не считаем
|
||||
QString line = origLine;
|
||||
line.remove(inlineCodeRe);
|
||||
// Не пропускаем заголовки целиком — только сам маркер "# " в начале,
|
||||
// теги после текста заголовка всё равно должны учитываться
|
||||
static const QRegularExpression headerMarkerRe(QStringLiteral("^\\s*#{1,6}\\s+"));
|
||||
line.remove(headerMarkerRe);
|
||||
auto it = tagRe.globalMatch(line);
|
||||
while (it.hasNext()) {
|
||||
const QString tag = it.next().captured(1);
|
||||
@@ -98,14 +105,33 @@ QString VaultIndex::resolveLink(const QString &fileDir, const QString &target) c
|
||||
return QString();
|
||||
if (t.contains(QLatin1String("://")))
|
||||
return QString(); // внешняя
|
||||
// Отсекаем якорь #section и query ?
|
||||
int hash = t.indexOf('#');
|
||||
if (hash >= 0) t = t.left(hash).trimmed();
|
||||
int q = t.indexOf('?');
|
||||
if (q >= 0) t = t.left(q).trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString();
|
||||
// Защита от traversal
|
||||
if (t.contains(QStringLiteral("..")))
|
||||
return QString();
|
||||
QStringList cands;
|
||||
cands << cleanDir(fileDir) + '/' + t
|
||||
<< cleanDir(fileDir) + '/' + t + ".md";
|
||||
for (const QString &c : cands)
|
||||
if (QFileInfo::exists(c))
|
||||
cands << QDir::cleanPath(cleanDir(fileDir) + '/' + t)
|
||||
<< QDir::cleanPath(cleanDir(fileDir) + '/' + t + ".md");
|
||||
for (const QString &c : cands) {
|
||||
if (QFileInfo::exists(c) && QFileInfo(c).isFile())
|
||||
return QDir::cleanPath(c);
|
||||
// поиск по имени файла по всему хранилищу (без расширения)
|
||||
auto it = m_fileByBase.constFind(t);
|
||||
}
|
||||
// поиск по имени файла по всему хранилищу (без расширения), case-insensitive
|
||||
const QString lower = t.toLower();
|
||||
auto it = m_fileByBase.constFind(lower);
|
||||
if (it != m_fileByBase.constEnd())
|
||||
return it.value();
|
||||
it = m_fileByBase.constFind(lower + ".md");
|
||||
if (it != m_fileByBase.constEnd())
|
||||
return it.value();
|
||||
// fallback точный регистр
|
||||
it = m_fileByBase.constFind(t);
|
||||
if (it != m_fileByBase.constEnd())
|
||||
return it.value();
|
||||
it = m_fileByBase.constFind(t + ".md");
|
||||
@@ -116,6 +142,20 @@ QString VaultIndex::resolveLink(const QString &fileDir, const QString &target) c
|
||||
|
||||
void VaultIndex::rebuild(const QString &vault)
|
||||
{
|
||||
if (vault.isEmpty() || !QDir(vault).exists()) {
|
||||
m_nodes.clear();
|
||||
m_edges.clear();
|
||||
m_nodeById.clear();
|
||||
m_tagsOfFile.clear();
|
||||
m_linksOfFile.clear();
|
||||
m_filesWithTag.clear();
|
||||
m_fileByBase.clear();
|
||||
m_fileCount = 0;
|
||||
emit indexChanged();
|
||||
return;
|
||||
}
|
||||
if (m_busy)
|
||||
return;
|
||||
m_busy = true;
|
||||
emit busyChanged();
|
||||
|
||||
@@ -134,12 +174,27 @@ void VaultIndex::rebuild(const QString &vault)
|
||||
m_fileCount = files.size();
|
||||
for (const QString &f : files) {
|
||||
const QString base = QFileInfo(f).completeBaseName();
|
||||
const QString key = base.toLower();
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(key))
|
||||
m_fileByBase.insert(key, f);
|
||||
// также храним оригинал для точного поиска
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(base))
|
||||
m_fileByBase.insert(base, f);
|
||||
}
|
||||
// Проход 2: узлы/теги/ссылки
|
||||
for (const QString &f : files)
|
||||
parseFile(f);
|
||||
// Дедупликация рёбер
|
||||
QSet<QString> seen;
|
||||
QVariantList uniq;
|
||||
for (const auto &e : std::as_const(m_edges)) {
|
||||
const QString k = e.toMap().value("from").toString() + "->" + e.toMap().value("to").toString();
|
||||
if (!seen.contains(k)) {
|
||||
seen.insert(k);
|
||||
uniq.append(e);
|
||||
}
|
||||
}
|
||||
m_edges = uniq;
|
||||
|
||||
m_busy = false;
|
||||
emit busyChanged();
|
||||
@@ -177,8 +232,13 @@ void VaultIndex::parseFile(const QString &path)
|
||||
ensureNode(key, QFileInfo(path).fileName(), QStringLiteral("file"));
|
||||
{
|
||||
const QString base = QFileInfo(path).completeBaseName();
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(base))
|
||||
m_fileByBase.insert(base, path);
|
||||
if (!base.isEmpty()) {
|
||||
const QString lower = base.toLower();
|
||||
if (!m_fileByBase.contains(lower))
|
||||
m_fileByBase.insert(lower, path);
|
||||
if (!m_fileByBase.contains(base))
|
||||
m_fileByBase.insert(base, path);
|
||||
}
|
||||
}
|
||||
|
||||
// Теги
|
||||
@@ -191,12 +251,19 @@ void VaultIndex::parseFile(const QString &path)
|
||||
m_filesWithTag[tag].append(path);
|
||||
}
|
||||
|
||||
// Ссылки [[X]] и [text](rel)
|
||||
// Ссылки [[X]] и [text](rel) (поддерживаем алиас | в wiki)
|
||||
QVariantList links;
|
||||
static const QRegularExpression wikiRe(QStringLiteral("\\[\\[([^\\]]+)\\]\\]"));
|
||||
auto wi = wikiRe.globalMatch(text);
|
||||
while (wi.hasNext()) {
|
||||
const QString target = wi.next().captured(1);
|
||||
QString target = wi.next().captured(1);
|
||||
// [[file|alias]] → берём file
|
||||
const int pipe = target.indexOf('|');
|
||||
if (pipe >= 0)
|
||||
target = target.left(pipe).trimmed();
|
||||
target = target.trimmed();
|
||||
if (target.isEmpty())
|
||||
continue;
|
||||
const QString resolved = resolveLink(fileDir, target);
|
||||
if (!resolved.isEmpty()) {
|
||||
links.append(QVariantMap{{"text", target}, {"path", resolved}});
|
||||
|
||||
Reference in New Issue
Block a user