Fix search

This commit is contained in:
J-Jamet
2019-07-22 11:26:47 +02:00
parent 2b387c8613
commit 7b67e4e811
8 changed files with 282 additions and 277 deletions

View File

@@ -37,7 +37,8 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.* import java.util.*
class SearchEntryCursorAdapter(context: Context, private val database: Database) : CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) { class SearchEntryCursorAdapter(context: Context, private val database: Database)
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService( private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
@@ -109,7 +110,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
} }
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? { override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntry(constraint.toString()) return database.searchEntries(constraint.toString())
} }
fun getEntryFromPosition(position: Int): EntryVersioned? { fun getEntryFromPosition(position: Int): EntryVersioned? {

View File

@@ -321,7 +321,7 @@ class Database {
return searchHelper?.search(this, str, max) return searchHelper?.search(this, str, max)
} }
fun searchEntry(query: String): Cursor? { fun searchEntries(query: String): Cursor? {
var cursorV3: EntryCursorV3? = null var cursorV3: EntryCursorV3? = null
var cursorV4: EntryCursorV4? = null var cursorV4: EntryCursorV4? = null

View File

@@ -166,7 +166,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
} }
/** /**
* Decode a reference key woth the SprEngineV4 * Decode a reference key with the SprEngineV4
* @param decodeRef * @param decodeRef
* @param key * @param key
* @return * @return
@@ -174,7 +174,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
private fun decodeRefKey(decodeRef: Boolean, key: String): String { private fun decodeRefKey(decodeRef: Boolean, key: String): String {
val text = fields.getProtectedStringValue(key) val text = fields.getProtectedStringValue(key)
return if (decodeRef) { return if (decodeRef) {
if (mDatabase == null) text else SprEngineV4().compile(text, this, mDatabase) if (mDatabase == null) text else SprEngineV4().compile(text, this, mDatabase!!)
} else text } else text
} }

View File

@@ -1,253 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element;
import com.kunzisoft.keepass.database.search.EntrySearchHandlerV4;
import com.kunzisoft.keepass.database.search.SearchParametersV4;
import com.kunzisoft.keepass.utils.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
public class SprEngineV4 {
private static final int MAX_RECURSION_DEPTH = 12;
private static final String STR_REF_START = "{REF:";
private static final String STR_REF_END = "}";
public class TargetResult {
public PwEntryV4 entry;
public char wanted;
public TargetResult(PwEntryV4 entry, char wanted) {
this.entry = entry;
this.wanted = wanted;
}
}
private class SprContextV4 {
public PwDatabaseV4 db;
public PwEntryV4 entry;
public Map<String, String> refsCache = new HashMap<>();
SprContextV4(PwDatabaseV4 db, PwEntryV4 entry) {
this.db = db;
this.entry = entry;
}
SprContextV4(SprContextV4 source) {
this.db = source.db;
this.entry = source.entry;
this.refsCache = source.refsCache;
}
}
public String compile(String text, PwEntryV4 entry, PwDatabase database) {
SprContextV4 ctx = new SprContextV4((PwDatabaseV4)database, entry);
return compileInternal(text, ctx, 0);
}
private String compileInternal(String text, SprContextV4 sprContextV4, int recursionLevel) {
if (text == null) { return ""; }
if (sprContextV4 == null) { return ""; }
if (recursionLevel >= MAX_RECURSION_DEPTH) { return ""; }
return fillRefPlaceholders(text, sprContextV4, recursionLevel);
}
private String fillRefPlaceholders(String text, SprContextV4 contextV4, int recursionLevel) {
if (contextV4.db == null) { return text; }
int offset = 0;
for (int i = 0; i < 20; ++i) {
text = fillRefsUsingCache(text, contextV4);
int start = StringUtil.INSTANCE.indexOfIgnoreCase(text, STR_REF_START, offset, Locale.ENGLISH);
if (start < 0) { break; }
int end = StringUtil.INSTANCE.indexOfIgnoreCase(text, STR_REF_END, start + 1, Locale.ENGLISH);
if (end <= start) { break; }
String fullRef = text.substring(start, end - start + 1);
TargetResult result = findRefTarget(fullRef, contextV4);
if (result != null) {
PwEntryV4 found = result.entry;
char wanted = result.wanted;
if (found != null) {
String data;
switch (wanted) {
case 'T':
data = found.getTitle();
break;
case 'U':
data = found.getUsername();
break;
case 'A':
data = found.getUrl();
break;
case 'P':
data = found.getPassword();
break;
case 'N':
data = found.getNotes();
break;
case 'I':
data = found.getNodeId().toString();
break;
default:
offset = start + 1;
continue;
}
SprContextV4 subCtx = new SprContextV4(contextV4);
subCtx.entry = found;
String innerContent = compileInternal(data, subCtx, recursionLevel + 1);
addRefsToCache(fullRef, innerContent, contextV4);
text = fillRefsUsingCache(text, contextV4);
} else {
offset = start + 1;
}
}
}
return text;
}
private TargetResult findRefTarget(String fullRef, SprContextV4 contextV4) {
if (fullRef == null) { return null; }
fullRef = fullRef.toUpperCase(Locale.ENGLISH);
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
return null;
}
String ref = fullRef.substring(STR_REF_START.length(), fullRef.length() - STR_REF_START.length() - STR_REF_END.length());
if (ref.length() <= 4) { return null; }
if (ref.charAt(1) != '@') { return null; }
if (ref.charAt(3) != ':') { return null; }
char scan = Character.toUpperCase(ref.charAt(2));
char wanted = Character.toUpperCase(ref.charAt(0));
SearchParametersV4 searchParametersV4 = new SearchParametersV4();
searchParametersV4.setupNone();
searchParametersV4.setSearchString(ref.substring(4));
if (scan == 'T') { searchParametersV4.setSearchInTitles(true); }
else if (scan == 'U') { searchParametersV4.setSearchInUserNames(true); }
else if (scan == 'A') { searchParametersV4.setSearchInUrls(true); }
else if (scan == 'P') { searchParametersV4.setSearchInPasswords(true); }
else if (scan == 'N') { searchParametersV4.setSearchInNotes(true); }
else if (scan == 'I') { searchParametersV4.setSearchInUUIDs(true); }
else if (scan == 'O') { searchParametersV4.setSearchInOther(true); }
else { return null; }
List<PwEntryV4> list = new ArrayList<>();
// TODO type parameter
searchEntries(contextV4.db.getRootGroup(), searchParametersV4, list);
if (list.size() > 0) {
return new TargetResult(list.get(0), wanted);
}
return null;
}
private void addRefsToCache(String ref, String value, SprContextV4 ctx) {
if (ref == null) { return; }
if (value == null) { return; }
if (ctx == null) { return; }
if (!ctx.refsCache.containsKey(ref)) {
ctx.refsCache.put(ref, value);
}
}
private String fillRefsUsingCache(String text, SprContextV4 sprContextV4) {
for (Entry<String, String> entry : sprContextV4.refsCache.entrySet()) {
text = StringUtil.INSTANCE.replaceAllIgnoresCase(text, entry.getKey(), entry.getValue(), Locale.ENGLISH);
}
return text;
}
private void searchEntries(PwGroupV4 root, SearchParametersV4 searchParametersV4, List<PwEntryV4> listStorage) {
if (searchParametersV4 == null) { return; }
if (listStorage == null) { return; }
List<String> terms = StringUtil.INSTANCE.splitStringTerms(searchParametersV4.getSearchString());
if (terms.size() <= 1 || searchParametersV4.getRegularExpression()) {
root.doForEachChild(new EntrySearchHandlerV4(searchParametersV4, listStorage), null);
return;
}
// Search longest term first
Comparator<String> stringLengthComparator = (lhs, rhs) -> lhs.length() - rhs.length();
Collections.sort(terms, stringLengthComparator);
String fullSearch = searchParametersV4.getSearchString();
List<PwEntryV4> childEntries = root.getChildEntries();
for (int i = 0; i < terms.size(); i ++) {
List<PwEntryV4> pgNew = new ArrayList<>();
searchParametersV4.setSearchString(terms.get(i));
boolean negate = false;
if (searchParametersV4.getSearchString().startsWith("-")) {
searchParametersV4.setSearchString(searchParametersV4.getSearchString().substring(1));
negate = searchParametersV4.getSearchString().length() > 0;
}
if (!root.doForEachChild(new EntrySearchHandlerV4(searchParametersV4, pgNew), null)) {
childEntries = null;
break;
}
List<PwEntryV4> complement = new ArrayList<>();
if (negate) {
for (PwEntryV4 entry: childEntries) {
if (!pgNew.contains(entry)) {
complement.add(entry);
}
}
childEntries = complement;
}
else {
childEntries = pgNew;
}
}
if (childEntries != null) {
listStorage.addAll(childEntries);
}
searchParametersV4.setSearchString(fullSearch);
}
}

View File

@@ -0,0 +1,258 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.database.search.EntrySearchHandlerV4
import com.kunzisoft.keepass.database.search.SearchParametersV4
import com.kunzisoft.keepass.utils.StringUtil
import java.util.*
class SprEngineV4 {
inner class TargetResult(var entry: PwEntryV4?, var wanted: Char)
private inner class SprContextV4 {
var databaseV4: PwDatabaseV4? = null
var entry: PwEntryV4
var refsCache: MutableMap<String, String> = HashMap()
internal constructor(db: PwDatabaseV4, entry: PwEntryV4) {
this.databaseV4 = db
this.entry = entry
}
internal constructor(source: SprContextV4) {
this.databaseV4 = source.databaseV4
this.entry = source.entry
this.refsCache = source.refsCache
}
}
fun compile(text: String, entry: PwEntryV4, database: PwDatabaseV4): String {
return compileInternal(text, SprContextV4(database, entry), 0)
}
private fun compileInternal(text: String?, sprContextV4: SprContextV4?, recursionLevel: Int): String {
if (text == null) {
return ""
}
if (sprContextV4 == null) {
return ""
}
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
""
} else fillRefPlaceholders(text, sprContextV4, recursionLevel)
}
private fun fillRefPlaceholders(text: String, contextV4: SprContextV4, recursionLevel: Int): String {
var text = text
if (contextV4.databaseV4 == null) {
return text
}
var offset = 0
for (i in 0..19) {
text = fillRefsUsingCache(text, contextV4)
val start = StringUtil.indexOfIgnoreCase(text!!, STR_REF_START, offset, Locale.ENGLISH)
if (start < 0) {
break
}
val end = StringUtil.indexOfIgnoreCase(text, STR_REF_END, start + 1, Locale.ENGLISH)
if (end <= start) {
break
}
val fullRef = text.substring(start, end - start + 1)
val result = findRefTarget(fullRef, contextV4)
if (result != null) {
val found = result.entry
val wanted = result.wanted
var data: String? = null
when (wanted) {
'T' -> data = found?.title
'U' -> data = found?.username
'A' -> data = found?.url
'P' -> data = found?.password
'N' -> data = found?.notes
'I' -> data = found?.nodeId.toString()
}
if (data != null && found != null) {
val subCtx = SprContextV4(contextV4)
subCtx.entry = found
val innerContent = compileInternal(data, subCtx, recursionLevel + 1)
addRefsToCache(fullRef, innerContent, contextV4)
text = fillRefsUsingCache(text, contextV4)
} else {
offset = start + 1
}
}
}
return text
}
private fun findRefTarget(fullRef: String?, contextV4: SprContextV4): TargetResult? {
var fullRef: String? = fullRef ?: return null
fullRef = fullRef!!.toUpperCase(Locale.ENGLISH)
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
return null
}
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_START.length - STR_REF_END.length)
if (ref.length <= 4) {
return null
}
if (ref[1] != '@') {
return null
}
if (ref[3] != ':') {
return null
}
val scan = Character.toUpperCase(ref[2])
val wanted = Character.toUpperCase(ref[0])
val searchParametersV4 = SearchParametersV4()
searchParametersV4.setupNone()
searchParametersV4.searchString = ref.substring(4)
if (scan == 'T') {
searchParametersV4.searchInTitles = true
} else if (scan == 'U') {
searchParametersV4.searchInUserNames = true
} else if (scan == 'A') {
searchParametersV4.searchInUrls = true
} else if (scan == 'P') {
searchParametersV4.searchInPasswords = true
} else if (scan == 'N') {
searchParametersV4.searchInNotes = true
} else if (scan == 'I') {
searchParametersV4.searchInUUIDs = true
} else if (scan == 'O') {
searchParametersV4.searchInOther = true
} else {
return null
}
val list = ArrayList<PwEntryV4>()
// TODO type parameter
searchEntries(contextV4.databaseV4!!.rootGroup, searchParametersV4, list)
return if (list.size > 0) {
TargetResult(list[0], wanted)
} else null
}
private fun addRefsToCache(ref: String?, value: String?, ctx: SprContextV4?) {
if (ref == null) {
return
}
if (value == null) {
return
}
if (ctx == null) {
return
}
if (!ctx.refsCache.containsKey(ref)) {
ctx.refsCache[ref] = value
}
}
private fun fillRefsUsingCache(text: String, sprContextV4: SprContextV4): String {
var newText = text
for ((key, value) in sprContextV4.refsCache) {
newText = StringUtil.replaceAllIgnoresCase(text, key, value, Locale.ENGLISH)
}
return newText
}
private fun searchEntries(root: PwGroupV4?, searchParametersV4: SearchParametersV4?, listStorage: MutableList<PwEntryV4>?) {
if (searchParametersV4 == null) {
return
}
if (listStorage == null) {
return
}
val terms = StringUtil.splitStringTerms(searchParametersV4.searchString)
if (terms.size <= 1 || searchParametersV4.regularExpression) {
root!!.doForEachChild(EntrySearchHandlerV4(searchParametersV4, listStorage), null)
return
}
// Search longest term first
val stringLengthComparator = Comparator<String> { lhs, rhs -> lhs.length - rhs.length }
Collections.sort(terms, stringLengthComparator)
val fullSearch = searchParametersV4.searchString
var childEntries: List<PwEntryV4>? = root!!.getChildEntries()
for (i in terms.indices) {
val pgNew = ArrayList<PwEntryV4>()
searchParametersV4.searchString = terms[i]
var negate = false
if (searchParametersV4.searchString.startsWith("-")) {
searchParametersV4.searchString = searchParametersV4.searchString.substring(1)
negate = searchParametersV4.searchString.length > 0
}
if (!root.doForEachChild(EntrySearchHandlerV4(searchParametersV4, pgNew), null)) {
childEntries = null
break
}
val complement = ArrayList<PwEntryV4>()
if (negate) {
for (entry in childEntries!!) {
if (!pgNew.contains(entry)) {
complement.add(entry)
}
}
childEntries = complement
} else {
childEntries = pgNew
}
}
if (childEntries != null) {
listStorage.addAll(childEntries)
}
searchParametersV4.searchString = fullSearch
}
companion object {
private const val MAX_RECURSION_DEPTH = 12
private const val STR_REF_START = "{REF:"
private const val STR_REF_END = "}"
}
}

View File

@@ -72,9 +72,13 @@ class SearchDbHelper(private val isOmitBackup: Boolean) {
return searchGroup return searchGroup
} }
private fun entryContainsString(entry: EntryVersioned, qStr: String, loc: Locale): Boolean { private fun entryContainsString(entry: EntryVersioned, searchString: String, locale: Locale): Boolean {
// Search all strings in the entry
// Entry don't contains string if the search string is empty
if (searchString.isEmpty())
return false
// Search all strings in the entry
var iterator: EntrySearchStringIterator? = null var iterator: EntrySearchStringIterator? = null
entry.pwEntryV3?.let { entry.pwEntryV3?.let {
iterator = EntrySearchStringIteratorV3(it) iterator = EntrySearchStringIteratorV3(it)
@@ -87,8 +91,7 @@ class SearchDbHelper(private val isOmitBackup: Boolean) {
while (it.hasNext()) { while (it.hasNext()) {
val str = it.next() val str = it.next()
if (str.isNotEmpty()) { if (str.isNotEmpty()) {
val lower = str.toLowerCase(loc) if (str.toLowerCase(locale).contains(searchString)) {
if (lower.contains(qStr)) {
return true return true
} }
} }

View File

@@ -29,7 +29,7 @@ class EntrySearchStringIteratorV4 : EntrySearchStringIterator {
private var mCurrent: String? = null private var mCurrent: String? = null
private var mSetIterator: Iterator<Entry<String, ProtectedString>>? = null private var mSetIterator: Iterator<Entry<String, ProtectedString>>? = null
private var mSearchParametersV4: SearchParametersV4? = null private var mSearchParametersV4: SearchParametersV4
constructor(entry: PwEntryV4) { constructor(entry: PwEntryV4) {
this.mSearchParametersV4 = SearchParametersV4() this.mSearchParametersV4 = SearchParametersV4()
@@ -52,9 +52,9 @@ class EntrySearchStringIteratorV4 : EntrySearchStringIterator {
throw NoSuchElementException("Past the end of the list.") throw NoSuchElementException("Past the end of the list.")
} }
val next = mCurrent val next:String = mCurrent!!
advance() advance()
return next!! return next
} }
private fun advance() { private fun advance() {
@@ -75,12 +75,12 @@ class EntrySearchStringIteratorV4 : EntrySearchStringIterator {
private fun searchInField(key: String): Boolean { private fun searchInField(key: String): Boolean {
return when (key) { return when (key) {
PwEntryV4.STR_TITLE -> mSearchParametersV4!!.searchInTitles PwEntryV4.STR_TITLE -> mSearchParametersV4.searchInTitles
PwEntryV4.STR_USERNAME -> mSearchParametersV4!!.searchInUserNames PwEntryV4.STR_USERNAME -> mSearchParametersV4.searchInUserNames
PwEntryV4.STR_PASSWORD -> mSearchParametersV4!!.searchInPasswords PwEntryV4.STR_PASSWORD -> mSearchParametersV4.searchInPasswords
PwEntryV4.STR_URL -> mSearchParametersV4!!.searchInUrls PwEntryV4.STR_URL -> mSearchParametersV4.searchInUrls
PwEntryV4.STR_NOTES -> mSearchParametersV4!!.searchInNotes PwEntryV4.STR_NOTES -> mSearchParametersV4.searchInNotes
else -> mSearchParametersV4!!.searchInOther else -> mSearchParametersV4.searchInOther
} }
} }

View File

@@ -69,14 +69,10 @@ object StringUtil {
return indexOfIgnoreCase(text, search, 0, locale) return indexOfIgnoreCase(text, search, 0, locale)
} }
fun replaceAllIgnoresCase(text: String?, find: String?, newText: String?, locale: Locale): String? { fun replaceAllIgnoresCase(text: String, find: String, newText: String, locale: Locale): String {
var currentText = text var currentText = text
if (currentText == null || find == null || newText == null) {
return currentText
}
var pos = 0 var pos = 0
while (pos < currentText!!.length) { while (pos < currentText.length) {
pos = indexOfIgnoreCase(currentText, find, pos, locale) pos = indexOfIgnoreCase(currentText, find, pos, locale)
if (pos < 0) { if (pos < 0) {
break break