diff --git a/autotests/dfm-search-tests/tst_recent_search_engine.cpp b/autotests/dfm-search-tests/tst_recent_search_engine.cpp index ea51ba7e..b4173345 100644 --- a/autotests/dfm-search-tests/tst_recent_search_engine.cpp +++ b/autotests/dfm-search-tests/tst_recent_search_engine.cpp @@ -45,6 +45,25 @@ QList createTestItems() }; } +// 包含黑名单目录下文件的测试数据,用于验证黑名单路径过滤 +QList createBlacklistTestItems() +{ + const qint64 now = QDateTime::currentDateTime().toSecsSinceEpoch(); + const qint64 yesterday = QDateTime::currentDateTime().addDays(-1).toSecsSinceEpoch(); + + return { + // 黑名单目录 /home/uos/Downloads 下的文件(应被过滤) + {"/home/uos/Downloads/report.pdf", "file:///home/uos/Downloads/report.pdf", now}, + {"/home/uos/Downloads/notes.txt", "file:///home/uos/Downloads/notes.txt", yesterday}, + {"/home/uos/Downloads/sub/deep.xlsx", "file:///home/uos/Downloads/sub/deep.xlsx", now}, + // 名称前缀相同但非黑名单子目录的文件(应保留——路径边界匹配) + {"/home/uos/Downloads_backup/archive.zip", "file:///home/uos/Downloads_backup/archive.zip", now}, + // 非黑名单目录下的文件(应保留) + {"/home/uos/Documents/plan.docx", "file:///home/uos/Documents/plan.docx", yesterday}, + {"/tmp/test/notes.txt", "file:///tmp/test/notes.txt", yesterday}, + }; +} + } // namespace // 可测试的 RecentSearchStrategy 子类,提供访问 fetchAddr 的接口 @@ -76,6 +95,10 @@ private Q_SLOTS: void search_detailedResults_containsExtendedAttributes(); void search_maxResults_truncatesResultCount(); void search_resultFoundEnabled_emitsSignalPerItem(); + void search_excludedPathsFilter_removesBlacklistedItems(); + void search_excludedPathsFilter_emptyKeepsAllItems(); + void search_excludedPathsCombinedWithKeyword_appliesBoth(); + void search_excludedPathsFilter_respectsPathBoundary(); }; void tst_RecentSearchEngine::initTestCase() @@ -328,6 +351,121 @@ void tst_RecentSearchEngine::search_resultFoundEnabled_emitsSignalPerItem() QCOMPARE(finishedSpy.count(), 1); } +void tst_RecentSearchEngine::search_excludedPathsFilter_removesBlacklistedItems() +{ + SearchOptions opts; + opts.setSearchExcludedPaths({"/home/uos/Downloads"}); + TestableRecentStrategy strategy(opts); + const auto testItems = createBlacklistTestItems(); + + stub_ext::StubExt stub; + stub.set_lamda(strategy.fetchAddr(), [testItems]() -> QList { + return testItems; + }); + + QSignalSpy finishedSpy(&strategy, &BaseSearchStrategy::searchFinished); + SearchQuery query = SearchFactory::createQuery(""); + strategy.search(query); + + QCOMPARE(finishedSpy.count(), 1); + SearchResultList results = finishedSpy.takeFirst().at(0).value(); + QStringList paths = resultPaths(results); + + // 黑名单目录 /home/uos/Downloads 下的 3 个文件被过滤; + // /home/uos/Downloads_backup/archive.zip 因路径边界匹配而保留,共 3 个 + QCOMPARE(paths.size(), 3); + QVERIFY(paths.contains("/home/uos/Documents/plan.docx")); + QVERIFY(paths.contains("/tmp/test/notes.txt")); + QVERIFY(paths.contains("/home/uos/Downloads_backup/archive.zip")); + QVERIFY(!paths.contains("/home/uos/Downloads/report.pdf")); + QVERIFY(!paths.contains("/home/uos/Downloads/notes.txt")); + QVERIFY(!paths.contains("/home/uos/Downloads/sub/deep.xlsx")); +} + +void tst_RecentSearchEngine::search_excludedPathsFilter_emptyKeepsAllItems() +{ + SearchOptions opts; + // 不设置黑名单路径 + TestableRecentStrategy strategy(opts); + const auto testItems = createBlacklistTestItems(); + + stub_ext::StubExt stub; + stub.set_lamda(strategy.fetchAddr(), [testItems]() -> QList { + return testItems; + }); + + QSignalSpy finishedSpy(&strategy, &BaseSearchStrategy::searchFinished); + SearchQuery query = SearchFactory::createQuery(""); + strategy.search(query); + + QCOMPARE(finishedSpy.count(), 1); + SearchResultList results = finishedSpy.takeFirst().at(0).value(); + // 无黑名单时全部保留 + QCOMPARE(results.size(), testItems.size()); +} + +void tst_RecentSearchEngine::search_excludedPathsCombinedWithKeyword_appliesBoth() +{ + SearchOptions opts; + opts.setSearchExcludedPaths({"/home/uos/Downloads"}); + TestableRecentStrategy strategy(opts); + const auto testItems = createBlacklistTestItems(); + + stub_ext::StubExt stub; + stub.set_lamda(strategy.fetchAddr(), [testItems]() -> QList { + return testItems; + }); + + QSignalSpy finishedSpy(&strategy, &BaseSearchStrategy::searchFinished); + // 关键词 "notes" 命中 /home/uos/Downloads/notes.txt(被黑名单过滤)与 /tmp/test/notes.txt(保留) + SearchQuery query = SearchFactory::createQuery("notes"); + strategy.search(query); + + QCOMPARE(finishedSpy.count(), 1); + SearchResultList results = finishedSpy.takeFirst().at(0).value(); + QStringList paths = resultPaths(results); + + QCOMPARE(paths.size(), 1); + QVERIFY(paths.contains("/tmp/test/notes.txt")); + QVERIFY(!paths.contains("/home/uos/Downloads/notes.txt")); +} + +void tst_RecentSearchEngine::search_excludedPathsFilter_respectsPathBoundary() +{ + // 验证路径边界匹配:黑名单 /home/uos/Downloads 不应误匹配 /home/uos/Downloads_backup + SearchOptions opts; + opts.setSearchExcludedPaths({"/home/uos/Downloads"}); + TestableRecentStrategy strategy(opts); + + const qint64 now = QDateTime::currentDateTime().toSecsSinceEpoch(); + const QList boundaryItems = { + // 黑名单目录下的文件(应过滤) + {"/home/uos/Downloads/file.txt", "file:///home/uos/Downloads/file.txt", now}, + // 名称前缀相同但非子目录(应保留) + {"/home/uos/Downloads_backup/file.txt", "file:///home/uos/Downloads_backup/file.txt", now}, + {"/home/uos/Downloads_archive/file.txt", "file:///home/uos/Downloads_archive/file.txt", now}, + }; + + stub_ext::StubExt stub; + stub.set_lamda(strategy.fetchAddr(), [boundaryItems]() -> QList { + return boundaryItems; + }); + + QSignalSpy finishedSpy(&strategy, &BaseSearchStrategy::searchFinished); + SearchQuery query = SearchFactory::createQuery(""); + strategy.search(query); + + QCOMPARE(finishedSpy.count(), 1); + SearchResultList results = finishedSpy.takeFirst().at(0).value(); + QStringList paths = resultPaths(results); + + // 仅 /home/uos/Downloads/file.txt 被过滤,_backup 和 _archive 因路径边界而保留 + QCOMPARE(paths.size(), 2); + QVERIFY(paths.contains("/home/uos/Downloads_backup/file.txt")); + QVERIFY(paths.contains("/home/uos/Downloads_archive/file.txt")); + QVERIFY(!paths.contains("/home/uos/Downloads/file.txt")); +} + QObject *create_tst_RecentSearchEngine() { return new tst_RecentSearchEngine(); diff --git a/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.cpp b/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.cpp index ed395311..3379cae8 100644 --- a/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.cpp +++ b/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.cpp @@ -12,8 +12,10 @@ #include #include #include +#include #include #include +#include DFM_SEARCH_BEGIN_NS @@ -136,6 +138,32 @@ QList RecentSearchStrategy::filterByTimeRange(const QList RecentSearchStrategy::filterByExcludedPaths(const QList &items) const +{ + QStringList excludedPaths = m_options.searchExcludedPaths(); + if (excludedPaths.isEmpty()) + return items; + + // 标准化路径:去除末尾斜杠、解析 "."/"..",确保路径边界匹配可靠。 + for (auto &p : excludedPaths) + p = QDir::cleanPath(p); + + QList out; + for (const RecentItem &item : std::as_const(items)) { + const bool excluded = std::any_of(excludedPaths.cbegin(), excludedPaths.cend(), + [&item](const QString &excludedPath) { + // 路径边界匹配:仅排除 excludedPath 自身或其子路径, + // 避免 /home/uos/Downloads 误匹配 /home/uos/Downloads_backup。 + return item.path == excludedPath + || item.path.startsWith(excludedPath + QLatin1Char('/')); + }); + if (!excluded) { + out.append(item); + } + } + return out; +} + // ── 结果构建 ─────────────────────────────────────────────────────── SearchResult RecentSearchStrategy::toSearchResult(const RecentItem &item) const @@ -180,7 +208,10 @@ void RecentSearchStrategy::search(const SearchQuery &query) return; } - // Step 2: 关键词过滤(仅 Simple 类型有 keyword;Boolean/Wildcard 暂不支持) + // Step 2: 黑名单路径过滤(尽早排除黑名单目录下的条目,减少后续过滤工作量) + items = filterByExcludedPaths(items); + + // Step 3: 关键词过滤(仅 Simple 类型有 keyword;Boolean/Wildcard 暂不支持) QString keyword; if (query.type() == SearchQuery::Type::Simple) { keyword = query.keyword(); @@ -190,17 +221,17 @@ void RecentSearchStrategy::search(const SearchQuery &query) } items = filterByKeyword(items, keyword); - // Step 3: 扩展名过滤 + // Step 4: 扩展名过滤 FileNameOptionsAPI optApi(const_cast(m_options)); const QStringList exts = optApi.fileExtensions(); items = filterByExtensions(items, exts); - // Step 4: 时间范围过滤(基于 modified 时间戳) + // Step 5: 时间范围过滤(基于 modified 时间戳) if (m_options.hasTimeRangeFilter()) { items = filterByTimeRange(items); } - // Step 5: 构建 SearchResult 并发射 + // Step 6: 构建 SearchResult 并发射 const bool resultFoundEnabled = m_options.resultFoundEnabled(); const int maxResults = m_options.maxResults() > 0 ? m_options.maxResults() : INT_MAX; diff --git a/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.h b/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.h index 44c45303..0146ca95 100644 --- a/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.h +++ b/src/dfm-search/dfm-search-lib/recentsearch/recentstrategies/recentsearchstrategy.h @@ -76,6 +76,17 @@ class RecentSearchStrategy : public BaseSearchStrategy */ QList filterByTimeRange(const QList &items) const; + /** + * @brief 按黑名单路径过滤。 + * + * 排除 path 等于或位于黑名单目录下的最近使用项, + * 采用路径边界匹配(excludedPath 或 excludedPath + "/"), + * 避免 /home/uos/Downloads 误匹配 /home/uos/Downloads_backup。 + * 与 FileNameRealTimeStrategy 的排除语义保持一致。 + * 空列表时不过滤。 + */ + QList filterByExcludedPaths(const QList &items) const; + /** * @brief 将 RecentItem 转换为 SearchResult,填充详细属性。 */