diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt index 0110b0027..764a1176c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt @@ -37,7 +37,8 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.settings.PreferencesUtil 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( Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater @@ -109,7 +110,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database) } override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? { - return database.searchEntry(constraint.toString()) + return database.searchEntries(constraint.toString()) } fun getEntryFromPosition(position: Int): EntryVersioned? { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 0ec5a7a4f..36ee53b0e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -321,7 +321,7 @@ class Database { return searchHelper?.search(this, str, max) } - fun searchEntry(query: String): Cursor? { + fun searchEntries(query: String): Cursor? { var cursorV3: EntryCursorV3? = null var cursorV4: EntryCursorV4? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt index 172e9fac9..db1e96cef 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt @@ -166,7 +166,7 @@ class PwEntryV4 : PwEntry, NodeV4Interface { } /** - * Decode a reference key woth the SprEngineV4 + * Decode a reference key with the SprEngineV4 * @param decodeRef * @param key * @return @@ -174,7 +174,7 @@ class PwEntryV4 : PwEntry, NodeV4Interface { private fun decodeRefKey(decodeRef: Boolean, key: String): String { val text = fields.getProtectedStringValue(key) 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 } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/SprEngineV4.java b/app/src/main/java/com/kunzisoft/keepass/database/element/SprEngineV4.java deleted file mode 100644 index 1cc1abc34..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/SprEngineV4.java +++ /dev/null @@ -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 . - * - */ -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 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 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 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 listStorage) { - if (searchParametersV4 == null) { return; } - if (listStorage == null) { return; } - - List 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 stringLengthComparator = (lhs, rhs) -> lhs.length() - rhs.length(); - Collections.sort(terms, stringLengthComparator); - - String fullSearch = searchParametersV4.getSearchString(); - List childEntries = root.getChildEntries(); - for (int i = 0; i < terms.size(); i ++) { - List 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 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); - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/SprEngineV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/SprEngineV4.kt new file mode 100644 index 000000000..378bee3ef --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/SprEngineV4.kt @@ -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 . + * + */ +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 = 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() + // 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?) { + 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 { lhs, rhs -> lhs.length - rhs.length } + Collections.sort(terms, stringLengthComparator) + + val fullSearch = searchParametersV4.searchString + var childEntries: List? = root!!.getChildEntries() + for (i in terms.indices) { + val pgNew = ArrayList() + + 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() + 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 = "}" + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt index f10d00120..5421d3739 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchDbHelper.kt @@ -72,9 +72,13 @@ class SearchDbHelper(private val isOmitBackup: Boolean) { return searchGroup } - private fun entryContainsString(entry: EntryVersioned, qStr: String, loc: Locale): Boolean { - // Search all strings in the entry + private fun entryContainsString(entry: EntryVersioned, searchString: String, locale: Locale): Boolean { + // 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 entry.pwEntryV3?.let { iterator = EntrySearchStringIteratorV3(it) @@ -87,8 +91,7 @@ class SearchDbHelper(private val isOmitBackup: Boolean) { while (it.hasNext()) { val str = it.next() if (str.isNotEmpty()) { - val lower = str.toLowerCase(loc) - if (lower.contains(qStr)) { + if (str.toLowerCase(locale).contains(searchString)) { return true } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/iterator/EntrySearchStringIteratorV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/iterator/EntrySearchStringIteratorV4.kt index 71f7453e2..ba72b0413 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/iterator/EntrySearchStringIteratorV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/iterator/EntrySearchStringIteratorV4.kt @@ -29,7 +29,7 @@ class EntrySearchStringIteratorV4 : EntrySearchStringIterator { private var mCurrent: String? = null private var mSetIterator: Iterator>? = null - private var mSearchParametersV4: SearchParametersV4? = null + private var mSearchParametersV4: SearchParametersV4 constructor(entry: PwEntryV4) { this.mSearchParametersV4 = SearchParametersV4() @@ -52,9 +52,9 @@ class EntrySearchStringIteratorV4 : EntrySearchStringIterator { throw NoSuchElementException("Past the end of the list.") } - val next = mCurrent + val next:String = mCurrent!! advance() - return next!! + return next } private fun advance() { @@ -75,12 +75,12 @@ class EntrySearchStringIteratorV4 : EntrySearchStringIterator { private fun searchInField(key: String): Boolean { return when (key) { - PwEntryV4.STR_TITLE -> mSearchParametersV4!!.searchInTitles - PwEntryV4.STR_USERNAME -> mSearchParametersV4!!.searchInUserNames - PwEntryV4.STR_PASSWORD -> mSearchParametersV4!!.searchInPasswords - PwEntryV4.STR_URL -> mSearchParametersV4!!.searchInUrls - PwEntryV4.STR_NOTES -> mSearchParametersV4!!.searchInNotes - else -> mSearchParametersV4!!.searchInOther + PwEntryV4.STR_TITLE -> mSearchParametersV4.searchInTitles + PwEntryV4.STR_USERNAME -> mSearchParametersV4.searchInUserNames + PwEntryV4.STR_PASSWORD -> mSearchParametersV4.searchInPasswords + PwEntryV4.STR_URL -> mSearchParametersV4.searchInUrls + PwEntryV4.STR_NOTES -> mSearchParametersV4.searchInNotes + else -> mSearchParametersV4.searchInOther } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt index 26763f011..492002718 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt @@ -69,14 +69,10 @@ object StringUtil { 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 - if (currentText == null || find == null || newText == null) { - return currentText - } - var pos = 0 - while (pos < currentText!!.length) { + while (pos < currentText.length) { pos = indexOfIgnoreCase(currentText, find, pos, locale) if (pos < 0) { break