Expose EntrySearcher's SearchTerm for internal code usage

This commit is contained in:
Aetf 2019-11-01 16:23:26 -04:00 committed by Jonathan White
parent 329701a34e
commit b96c1e92a3
3 changed files with 97 additions and 67 deletions

View file

@ -28,6 +28,20 @@ EntrySearcher::EntrySearcher(bool caseSensitive)
{ {
} }
/**
* Search group, and its children, directly by provided search terms
* @param searchTerms search terms
* @param baseGroup group to start search from, cannot be null
* @param forceSearch ignore group search settings
* @return list of entries that match the search terms
*/
QList<Entry*> EntrySearcher::search(const QList<SearchTerm>& searchTerms, const Group* baseGroup, bool forceSearch)
{
Q_ASSERT(baseGroup);
m_searchTerms = searchTerms;
return repeat(baseGroup, forceSearch);
}
/** /**
* Search group, and its children, by parsing the provided search * Search group, and its children, by parsing the provided search
* string for search terms. * string for search terms.
@ -69,6 +83,19 @@ QList<Entry*> EntrySearcher::repeat(const Group* baseGroup, bool forceSearch)
return results; return results;
} }
/**
* Search provided entries by the provided search terms
*
* @param searchTerms search terms
* @param entries list of entries to include in the search
* @return list of entries that match the search terms
*/
QList<Entry*> EntrySearcher::searchEntries(const QList<SearchTerm>& searchTerms, const QList<Entry*>& entries)
{
m_searchTerms = searchTerms;
return repeatEntries(entries);
}
/** /**
* Search provided entries by parsing the search string * Search provided entries by parsing the search string
* for search terms. * for search terms.
@ -124,46 +151,46 @@ bool EntrySearcher::searchEntryImpl(Entry* entry)
bool found; bool found;
for (const auto& term : m_searchTerms) { for (const auto& term : m_searchTerms) {
switch (term->field) { switch (term.field) {
case Field::Title: case Field::Title:
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch(); found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch();
break; break;
case Field::Username: case Field::Username:
found = term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch(); found = term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch();
break; break;
case Field::Password: case Field::Password:
found = term->regex.match(entry->resolvePlaceholder(entry->password())).hasMatch(); found = term.regex.match(entry->resolvePlaceholder(entry->password())).hasMatch();
break; break;
case Field::Url: case Field::Url:
found = term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch(); found = term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch();
break; break;
case Field::Notes: case Field::Notes:
found = term->regex.match(entry->notes()).hasMatch(); found = term.regex.match(entry->notes()).hasMatch();
break; break;
case Field::AttributeKey: case Field::AttributeKV:
found = !attributes.filter(term->regex).empty(); found = !attributes.filter(term.regex).empty();
break; break;
case Field::Attachment: case Field::Attachment:
found = !attachments.filter(term->regex).empty(); found = !attachments.filter(term.regex).empty();
break; break;
case Field::AttributeValue: case Field::AttributeValue:
// skip protected attributes // skip protected attributes
if (entry->attributes()->isProtected(term->word)) { if (entry->attributes()->isProtected(term.word)) {
continue; continue;
} }
found = entry->attributes()->contains(term->word) found = entry->attributes()->contains(term.word)
&& term->regex.match(entry->attributes()->value(term->word)).hasMatch(); && term.regex.match(entry->attributes()->value(term.word)).hasMatch();
break; break;
default: default:
// Terms without a specific field try to match title, username, url, and notes // Terms without a specific field try to match title, username, url, and notes
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch()
|| term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch() || term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch()
|| term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch() || term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch()
|| term->regex.match(entry->notes()).hasMatch(); || term.regex.match(entry->notes()).hasMatch();
} }
// Short circuit if we failed to match or we matched and are excluding this term // Short circuit if we failed to match or we matched and are excluding this term
if ((!found && !term->exclude) || (found && term->exclude)) { if ((!found && !term.exclude) || (found && term.exclude)) {
return false; return false;
} }
} }
@ -175,7 +202,7 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
{ {
static const QList<QPair<QString, Field>> fieldnames{ static const QList<QPair<QString, Field>> fieldnames{
{QStringLiteral("attachment"), Field::Attachment}, {QStringLiteral("attachment"), Field::Attachment},
{QStringLiteral("attribute"), Field::AttributeKey}, {QStringLiteral("attribute"), Field::AttributeKV},
{QStringLiteral("notes"), Field::Notes}, {QStringLiteral("notes"), Field::Notes},
{QStringLiteral("pw"), Field::Password}, {QStringLiteral("pw"), Field::Password},
{QStringLiteral("password"), Field::Password}, {QStringLiteral("password"), Field::Password},
@ -188,44 +215,44 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
auto results = m_termParser.globalMatch(searchString); auto results = m_termParser.globalMatch(searchString);
while (results.hasNext()) { while (results.hasNext()) {
auto result = results.next(); auto result = results.next();
auto term = QSharedPointer<SearchTerm>::create(); SearchTerm term{};
// Quoted string group // Quoted string group
term->word = result.captured(3); term.word = result.captured(3);
// If empty, use the unquoted string group // If empty, use the unquoted string group
if (term->word.isEmpty()) { if (term.word.isEmpty()) {
term->word = result.captured(4); term.word = result.captured(4);
} }
// If still empty, ignore this match // If still empty, ignore this match
if (term->word.isEmpty()) { if (term.word.isEmpty()) {
continue; continue;
} }
auto mods = result.captured(1); auto mods = result.captured(1);
// Convert term to regex // Convert term to regex
term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive); term.regex = Tools::convertToRegex(term.word, !mods.contains("*"), mods.contains("+"), m_caseSensitive);
// Exclude modifier // Exclude modifier
term->exclude = mods.contains("-") || mods.contains("!"); term.exclude = mods.contains("-") || mods.contains("!");
// Determine the field to search // Determine the field to search
term->field = Field::Undefined; term.field = Field::Undefined;
QString field = result.captured(2); QString field = result.captured(2);
if (!field.isEmpty()) { if (!field.isEmpty()) {
if (field.startsWith("_", Qt::CaseInsensitive)) { if (field.startsWith("_", Qt::CaseInsensitive)) {
term->field = Field::AttributeValue; term.field = Field::AttributeValue;
// searching a custom attribute // searching a custom attribute
// in this case term->word is the attribute key (removing the leading "_") // in this case term.word is the attribute key (removing the leading "_")
// and term->regex is used to match attribute value // and term.regex is used to match attribute value
term->word = field.mid(1); term.word = field.mid(1);
} else { } else {
for (const auto& pair : fieldnames) { for (const auto& pair : fieldnames) {
if (pair.first.startsWith(field, Qt::CaseInsensitive)) { if (pair.first.startsWith(field, Qt::CaseInsensitive)) {
term->field = pair.second; term.field = pair.second;
break; break;
} }
} }

View file

@ -28,18 +28,6 @@ class Entry;
class EntrySearcher class EntrySearcher
{ {
public: public:
explicit EntrySearcher(bool caseSensitive = false);
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
QList<Entry*> repeat(const Group* baseGroup, bool forceSearch = false);
QList<Entry*> searchEntries(const QString& searchString, const QList<Entry*>& entries);
QList<Entry*> repeatEntries(const QList<Entry*>& entries);
void setCaseSensitive(bool state);
bool isCaseSensitive();
private:
enum class Field enum class Field
{ {
Undefined, Undefined,
@ -48,7 +36,7 @@ private:
Password, Password,
Url, Url,
Notes, Notes,
AttributeKey, AttributeKV,
Attachment, Attachment,
AttributeValue AttributeValue
}; };
@ -56,17 +44,32 @@ private:
struct SearchTerm struct SearchTerm
{ {
Field field; Field field;
// only used when field == Field::AttributeValue
QString word; QString word;
QRegularExpression regex; QRegularExpression regex;
bool exclude; bool exclude;
}; };
explicit EntrySearcher(bool caseSensitive = false);
QList<Entry*> search(const QList<SearchTerm>& searchTerms, const Group* baseGroup, bool forceSearch = false);
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
QList<Entry*> repeat(const Group* baseGroup, bool forceSearch = false);
QList<Entry*> searchEntries(const QList<SearchTerm>& searchTerms, const QList<Entry*>& entries);
QList<Entry*> searchEntries(const QString& searchString, const QList<Entry*>& entries);
QList<Entry*> repeatEntries(const QList<Entry*>& entries);
void setCaseSensitive(bool state);
bool isCaseSensitive();
private:
bool searchEntryImpl(Entry* entry); bool searchEntryImpl(Entry* entry);
void parseSearchTerms(const QString& searchString); void parseSearchTerms(const QString& searchString);
bool m_caseSensitive; bool m_caseSensitive;
QRegularExpression m_termParser; QRegularExpression m_termParser;
QList<QSharedPointer<SearchTerm>> m_searchTerms; QList<SearchTerm> m_searchTerms;
friend class TestEntrySearcher; friend class TestEntrySearcher;
}; };

View file

@ -197,22 +197,22 @@ void TestEntrySearcher::testSearchTermParser()
QCOMPARE(terms.length(), 5); QCOMPARE(terms.length(), 5);
QCOMPARE(terms[0]->field, EntrySearcher::Field::Undefined); QCOMPARE(terms[0].field, EntrySearcher::Field::Undefined);
QCOMPARE(terms[0]->word, QString("test")); QCOMPARE(terms[0].word, QString("test"));
QCOMPARE(terms[0]->exclude, true); QCOMPARE(terms[0].exclude, true);
QCOMPARE(terms[1]->field, EntrySearcher::Field::Undefined); QCOMPARE(terms[1].field, EntrySearcher::Field::Undefined);
QCOMPARE(terms[1]->word, QString("quoted \\\"string\\\"")); QCOMPARE(terms[1].word, QString("quoted \\\"string\\\""));
QCOMPARE(terms[1]->exclude, false); QCOMPARE(terms[1].exclude, false);
QCOMPARE(terms[2]->field, EntrySearcher::Field::Username); QCOMPARE(terms[2].field, EntrySearcher::Field::Username);
QCOMPARE(terms[2]->word, QString("user")); QCOMPARE(terms[2].word, QString("user"));
QCOMPARE(terms[3]->field, EntrySearcher::Field::Password); QCOMPARE(terms[3].field, EntrySearcher::Field::Password);
QCOMPARE(terms[3]->word, QString("test me")); QCOMPARE(terms[3].word, QString("test me"));
QCOMPARE(terms[4]->field, EntrySearcher::Field::Undefined); QCOMPARE(terms[4].field, EntrySearcher::Field::Undefined);
QCOMPARE(terms[4]->word, QString("noquote")); QCOMPARE(terms[4].word, QString("noquote"));
// Test wildcard and regex search terms // Test wildcard and regex search terms
m_entrySearcher.parseSearchTerms("+url:*.google.com *user:\\d+\\w{2}"); m_entrySearcher.parseSearchTerms("+url:*.google.com *user:\\d+\\w{2}");
@ -220,11 +220,11 @@ void TestEntrySearcher::testSearchTermParser()
QCOMPARE(terms.length(), 2); QCOMPARE(terms.length(), 2);
QCOMPARE(terms[0]->field, EntrySearcher::Field::Url); QCOMPARE(terms[0].field, EntrySearcher::Field::Url);
QCOMPARE(terms[0]->regex.pattern(), QString("^.*\\.google\\.com$")); QCOMPARE(terms[0].regex.pattern(), QString("^.*\\.google\\.com$"));
QCOMPARE(terms[1]->field, EntrySearcher::Field::Username); QCOMPARE(terms[1].field, EntrySearcher::Field::Username);
QCOMPARE(terms[1]->regex.pattern(), QString("\\d+\\w{2}")); QCOMPARE(terms[1].regex.pattern(), QString("\\d+\\w{2}"));
// Test custom attribute search terms // Test custom attribute search terms
m_entrySearcher.parseSearchTerms("+_abc:efg _def:\"ddd\""); m_entrySearcher.parseSearchTerms("+_abc:efg _def:\"ddd\"");
@ -232,13 +232,13 @@ void TestEntrySearcher::testSearchTermParser()
QCOMPARE(terms.length(), 2); QCOMPARE(terms.length(), 2);
QCOMPARE(terms[0]->field, EntrySearcher::Field::AttributeValue); QCOMPARE(terms[0].field, EntrySearcher::Field::AttributeValue);
QCOMPARE(terms[0]->word, QString("abc")); QCOMPARE(terms[0].word, QString("abc"));
QCOMPARE(terms[0]->regex.pattern(), QString("^efg$")); QCOMPARE(terms[0].regex.pattern(), QString("^efg$"));
QCOMPARE(terms[1]->field, EntrySearcher::Field::AttributeValue); QCOMPARE(terms[1].field, EntrySearcher::Field::AttributeValue);
QCOMPARE(terms[1]->word, QString("def")); QCOMPARE(terms[1].word, QString("def"));
QCOMPARE(terms[1]->regex.pattern(), QString("ddd")); QCOMPARE(terms[1].regex.pattern(), QString("ddd"));
} }
void TestEntrySearcher::testCustomAttributesAreSearched() void TestEntrySearcher::testCustomAttributesAreSearched()