Fix subdomain search #728

This commit is contained in:
J-Jamet
2020-10-22 21:16:44 +02:00
parent 6a935a49ea
commit 110e2b7580
11 changed files with 586 additions and 101 deletions

Binary file not shown.

View File

@@ -40,6 +40,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.UriUtil
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() { class AutofillLauncherActivity : AppCompatActivity() {
@@ -61,83 +62,26 @@ class AutofillLauncherActivity : AppCompatActivity() {
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
} }
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) if (!PreferencesUtil.searchSubdomains(this)) {
val assistStructure = AutofillHelper.retrieveAssistStructure(intent) UriUtil.getWebDomainWithoutSubDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
if (assistStructure == null) { launchSelection(searchInfo)
setResult(Activity.RESULT_CANCELED) }
finish()
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
finish()
} else { } else {
// If database is open launchSelection(searchInfo)
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Items found
AutofillHelper.buildResponse(this, items)
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
sendBroadcast(Intent(LOCK_ACTION))
}
finish()
},
{
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
assistStructure,
false,
searchInfo)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure,
searchInfo)
}
)
} }
} }
SpecialMode.REGISTRATION -> { SpecialMode.REGISTRATION -> {
// To register info // To register info
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO) val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
val searchInfo = SearchInfo(registerInfo?.searchInfo) val searchInfo = SearchInfo(registerInfo?.searchInfo)
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, if (!PreferencesUtil.searchSubdomains(this)) {
PreferencesUtil.applicationIdBlocklist(this)) UriUtil.getWebDomainWithoutSubDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, searchInfo.webDomain = webDomainWithoutSubDomain
PreferencesUtil.webDomainBlocklist(this))) { launchRegistration(searchInfo, registerInfo)
// If item not allowed, show a toast }
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
} else {
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ _ ->
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
registerInfo)
},
{
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
registerInfo)
},
{
// If database not open
FileDatabaseSelectActivity.launchForRegistration(this,
registerInfo)
}
)
} }
finish() launchRegistration(searchInfo, registerInfo)
} }
} }
} }
@@ -145,6 +89,84 @@ class AutofillLauncherActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
private fun launchSelection(searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure == null) {
setResult(Activity.RESULT_CANCELED)
finish()
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
finish()
} else {
// If database is open
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Items found
AutofillHelper.buildResponse(this, items)
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
sendBroadcast(Intent(LOCK_ACTION))
}
finish()
},
{
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
assistStructure,
false,
searchInfo)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure,
searchInfo)
}
)
}
}
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
} else {
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ _ ->
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
registerInfo)
},
{
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
registerInfo)
},
{
// If database not open
FileDatabaseSelectActivity.launchForRegistration(this,
registerInfo)
}
)
}
finish()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)

View File

@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
/** /**
* Activity to search or select entry in database, * Activity to search or select entry in database,
@@ -54,13 +55,25 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
else -> {} else -> {}
} }
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// Build search param // Build search param
val searchInfo = SearchInfo().apply { val searchInfo = SearchInfo().apply {
webDomain = sharedWebDomain webDomain = sharedWebDomain
} }
if (!PreferencesUtil.searchSubdomains(this)) {
UriUtil.getWebDomainWithoutSubDomain(this, sharedWebDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
launch(searchInfo)
}
} else {
launch(searchInfo)
}
super.onCreate(savedInstanceState)
}
private fun launch(searchInfo: SearchInfo) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open // If database is open
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
@@ -112,8 +125,6 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
) )
finish() finish()
super.onCreate(savedInstanceState)
} }
} }

View File

@@ -65,7 +65,6 @@ import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.getSearchString
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
@@ -364,7 +363,7 @@ class GroupActivity : LockingActivity(),
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false) val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
if (searchInfo != null && autoSearch) { if (searchInfo != null && autoSearch) {
intent.action = Intent.ACTION_SEARCH intent.action = Intent.ACTION_SEARCH
intent.putExtra(SearchManager.QUERY, searchInfo.getSearchString(this)) intent.putExtra(SearchManager.QUERY, searchInfo.toString())
return true return true
} }
return false return false

View File

@@ -28,7 +28,6 @@ import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorK
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.getSearchString
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -53,7 +52,7 @@ class SearchHelper {
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()) {
// If search provide results // If search provide results
database.createVirtualGroupFromSearchInfo( database.createVirtualGroupFromSearchInfo(
searchInfo.getSearchString(context), searchInfo.toString(),
PreferencesUtil.omitBackup(context), PreferencesUtil.omitBackup(context),
MAX_SEARCH_ENTRY MAX_SEARCH_ENTRY
)?.let { searchGroup -> )?.let { searchGroup ->

View File

@@ -1,12 +1,9 @@
package com.kunzisoft.keepass.model package com.kunzisoft.keepass.model
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.ObjectNameResource import com.kunzisoft.keepass.utils.ObjectNameResource
import com.kunzisoft.keepass.utils.UriUtil
class SearchInfo : ObjectNameResource, Parcelable { class SearchInfo : ObjectNameResource, Parcelable {
@@ -106,15 +103,4 @@ class SearchInfo : ObjectNameResource, Parcelable {
} }
} }
} }
}
fun SearchInfo.getSearchString(context: Context): String {
return run {
if (!PreferencesUtil.searchSubdomains(context))
UriUtil.getWebDomainWithoutSubDomain(webDomain)
else
webDomain
}
?: applicationId
?: ""
} }

View File

@@ -27,6 +27,10 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import java.io.* import java.io.*
import java.util.* import java.util.*
@@ -94,17 +98,18 @@ object UriUtil {
null null
} }
fun getWebDomainWithoutSubDomain(webDomain: String?): String? { fun getWebDomainWithoutSubDomain(context: Context,
webDomain?.split(".")?.let { domainArray -> webDomain: String?,
if (domainArray.isEmpty()) { webDomainWithoutSubDomain: (String?) -> Unit) {
return "" CoroutineScope(Dispatchers.Main).launch {
if (webDomain != null) {
val publicSuffixList = PublicSuffixList(context)
webDomainWithoutSubDomain.invoke(publicSuffixList
.getPublicSuffixPlusOne(webDomain).await())
} else {
webDomainWithoutSubDomain.invoke(null)
} }
if (domainArray.size == 1) {
return domainArray[0];
}
return domainArray[domainArray.size - 2] + "." + domainArray[domainArray.size - 1]
} }
return null
} }
fun decode(uri: String?): String { fun decode(uri: String?): String {

View File

@@ -0,0 +1,135 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist
import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
/**
* API for reading and accessing the public suffix list.
*
* > A "public suffix" is one under which Internet users can (or historically could) directly register names. Some
* > examples of public suffixes are .com, .co.uk and pvt.k12.ma.us. The Public Suffix List is a list of all known
* > public suffixes.
*
* Note that this implementation applies the rules of the public suffix list only and does not validate domains.
*
* https://publicsuffix.org/
* https://github.com/publicsuffix/list
*/
class PublicSuffixList(
context: Context,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val scope: CoroutineScope = CoroutineScope(dispatcher)
) {
private val data: PublicSuffixListData by lazy { PublicSuffixListLoader.load(context) }
/**
* Prefetch the public suffix list from disk so that it is available in memory.
*/
fun prefetch(): Deferred<Unit> = scope.async {
data.run { Unit }
}
/**
* Returns true if the given [domain] is a public suffix; false otherwise.
*
* E.g.:
* ```
* co.uk -> true
* com -> true
* mozilla.org -> false
* org -> true
* ```
*
* Note that this method ignores the default "prevailing rule" described in the formal public suffix list algorithm:
* If no rule matches then the passed [domain] is assumed to *not* be a public suffix.
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun isPublicSuffix(domain: String): Deferred<Boolean> = scope.async {
when (data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.PublicSuffix -> true
else -> false
}
}
/**
* Returns the public suffix and one more level; known as the registrable domain. Returns `null` if
* [domain] is a public suffix itself.
*
* E.g.:
* ```
* wwww.mozilla.org -> mozilla.org
* www.bcc.co.uk -> bbc.co.uk
* a.b.ide.kyoto.jp -> b.ide.kyoto.jp
* ```
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun getPublicSuffixPlusOne(domain: String): Deferred<String?> = scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain
.split('.')
.drop(offset.value)
.joinToString(separator = ".")
else -> null
}
}
/**
* Returns the public suffix of the given [domain]; known as the effective top-level domain (eTLD). Returns `null`
* if the [domain] is a public suffix itself.
*
* E.g.:
* ```
* wwww.mozilla.org -> org
* www.bcc.co.uk -> co.uk
* a.b.ide.kyoto.jp -> ide.kyoto.jp
* ```
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun getPublicSuffix(domain: String) = scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain
.split('.')
.drop(offset.value + 1)
.joinToString(separator = ".")
else -> null
}
}
/**
* Strips the public suffix from the given [domain]. Returns the original domain if no public suffix could be
* stripped.
*
* E.g.:
* ```
* wwww.mozilla.org -> www.mozilla
* www.bcc.co.uk -> www.bbc
* a.b.ide.kyoto.jp -> a.b
* ```
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun stripPublicSuffix(domain: String) = scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain
.split('.')
.joinToString(separator = ".", limit = offset.value + 1, truncated = "")
.dropLast(1)
else -> domain
}
}
}

View File

@@ -0,0 +1,158 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist
import mozilla.components.lib.publicsuffixlist.ext.binarySearch
import java.net.IDN
/**
* Class wrapping the public suffix list data and offering methods for accessing rules in it.
*/
internal class PublicSuffixListData(
private val rules: ByteArray,
private val exceptions: ByteArray
) {
private fun binarySearchRules(labels: List<ByteArray>, labelIndex: Int): String? {
return rules.binarySearch(labels, labelIndex)
}
private fun binarySearchExceptions(labels: List<ByteArray>, labelIndex: Int): String? {
return exceptions.binarySearch(labels, labelIndex)
}
@Suppress("ReturnCount")
fun getPublicSuffixOffset(domain: String): PublicSuffixOffset? {
if (domain.isEmpty()) {
return null
}
val domainLabels = IDN.toUnicode(domain).split('.')
if (domainLabels.find { it.isEmpty() } != null) {
// At least one of the labels is empty: Bail out.
return null
}
val rule = findMatchingRule(domainLabels)
if (domainLabels.size == rule.size && rule[0][0] != PublicSuffixListData.EXCEPTION_MARKER) {
// The domain is a public suffix.
return if (rule == PublicSuffixListData.PREVAILING_RULE) {
PublicSuffixOffset.PrevailingRule
} else {
PublicSuffixOffset.PublicSuffix
}
}
return if (rule[0][0] == PublicSuffixListData.EXCEPTION_MARKER) {
// Exception rules hold the effective TLD plus one.
PublicSuffixOffset.Offset(domainLabels.size - rule.size)
} else {
// Otherwise the rule is for a public suffix, so we must take one more label.
PublicSuffixOffset.Offset(domainLabels.size - (rule.size + 1))
}
}
/**
* Find a matching rule for the given domain labels.
*
* This algorithm is based on OkHttp's PublicSuffixDatabase class:
* https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
*/
private fun findMatchingRule(domainLabels: List<String>): List<String> {
// Break apart the domain into UTF-8 labels, i.e. foo.bar.com turns into [foo, bar, com].
val domainLabelsBytes = domainLabels.map { it.toByteArray(Charsets.UTF_8) }
val exactMatch = findExactMatch(domainLabelsBytes)
val wildcardMatch = findWildcardMatch(domainLabelsBytes)
val exceptionMatch = findExceptionMatch(domainLabelsBytes, wildcardMatch)
if (exceptionMatch != null) {
return ("${PublicSuffixListData.EXCEPTION_MARKER}$exceptionMatch").split('.')
}
if (exactMatch == null && wildcardMatch == null) {
return PublicSuffixListData.PREVAILING_RULE
}
val exactRuleLabels = exactMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
val wildcardRuleLabels = wildcardMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
return if (exactRuleLabels.size > wildcardRuleLabels.size) {
exactRuleLabels
} else {
wildcardRuleLabels
}
}
/**
* Returns an exact match or null.
*/
private fun findExactMatch(labels: List<ByteArray>): String? {
// Start by looking for exact matches. We start at the leftmost label. For example, foo.bar.com
// will look like: [foo, bar, com], [bar, com], [com]. The longest matching rule wins.
for (i in 0 until labels.size) {
val rule = binarySearchRules(labels, i)
if (rule != null) {
return rule
}
}
return null
}
/**
* Returns a wildcard match or null.
*/
private fun findWildcardMatch(labels: List<ByteArray>): String? {
// In theory, wildcard rules are not restricted to having the wildcard in the leftmost position.
// In practice, wildcards are always in the leftmost position. For now, this implementation
// cheats and does not attempt every possible permutation. Instead, it only considers wildcards
// in the leftmost position. We assert this fact when we generate the public suffix file. If
// this assertion ever fails we'll need to refactor this implementation.
if (labels.size > 1) {
val labelsWithWildcard = labels.toMutableList()
for (labelIndex in 0 until labelsWithWildcard.size) {
labelsWithWildcard[labelIndex] = PublicSuffixListData.WILDCARD_LABEL
val rule = binarySearchRules(labelsWithWildcard, labelIndex)
if (rule != null) {
return rule
}
}
}
return null
}
private fun findExceptionMatch(labels: List<ByteArray>, wildcardMatch: String?): String? {
// Exception rules only apply to wildcard rules, so only try it if we matched a wildcard.
if (wildcardMatch == null) {
return null
}
for (labelIndex in 0 until labels.size) {
val rule = binarySearchExceptions(labels, labelIndex)
if (rule != null) {
return rule
}
}
return null
}
companion object {
val WILDCARD_LABEL = byteArrayOf('*'.toByte())
val PREVAILING_RULE = listOf("*")
val EMPTY_RULE = listOf<String>()
const val EXCEPTION_MARKER = '!'
}
}
internal sealed class PublicSuffixOffset {
data class Offset(val value: Int) : PublicSuffixOffset()
object PublicSuffix : PublicSuffixOffset()
object PrevailingRule : PublicSuffixOffset()
}

View File

@@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist
import android.content.Context
import java.io.BufferedInputStream
import java.io.IOException
private const val PUBLIC_SUFFIX_LIST_FILE = "publicsuffixes"
internal object PublicSuffixListLoader {
fun load(context: Context): PublicSuffixListData = context.assets.open(
PUBLIC_SUFFIX_LIST_FILE
).buffered().use { stream ->
val publicSuffixSize = stream.readInt()
val publicSuffixBytes = stream.readFully(publicSuffixSize)
val exceptionSize = stream.readInt()
val exceptionBytes = stream.readFully(exceptionSize)
PublicSuffixListData(publicSuffixBytes, exceptionBytes)
}
}
@Suppress("MagicNumber")
private fun BufferedInputStream.readInt(): Int {
return (read() and 0xff shl 24
or (read() and 0xff shl 16)
or (read() and 0xff shl 8)
or (read() and 0xff))
}
private fun BufferedInputStream.readFully(size: Int): ByteArray {
val bytes = ByteArray(size)
var offset = 0
while (offset < size) {
val read = read(bytes, offset, size - offset)
if (read == -1) {
throw IOException("Unexpected end of stream")
}
offset += read
}
return bytes
}

View File

@@ -0,0 +1,122 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist.ext
import kotlin.experimental.and
private const val BITMASK = 0xff.toByte()
/**
* Performs a binary search for the provided [labels] on the [ByteArray]'s data.
*
* This algorithm is based on OkHttp's PublicSuffixDatabase class:
* https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
*/
@Suppress("ComplexMethod", "NestedBlockDepth")
internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): String? {
var low = 0
var high = size
var match: String? = null
while (low < high) {
val mid = (low + high) / 2
val start = findStartOfLineFromIndex(mid)
val end = findEndOfLineFromIndex(start)
val publicSuffixLength = start + end - start
var compareResult: Int
var currentLabelIndex = labelIndex
var currentLabelByteIndex = 0
var publicSuffixByteIndex = 0
var expectDot = false
while (true) {
val byte0 = if (expectDot) {
expectDot = false
'.'.toByte()
} else {
labels[currentLabelIndex][currentLabelByteIndex] and BITMASK
}
val byte1 = this[start + publicSuffixByteIndex] and BITMASK
// Compare the bytes. Note that the file stores UTF-8 encoded bytes, so we must compare the
// unsigned bytes.
@Suppress("EXPERIMENTAL_API_USAGE")
compareResult = (byte0.toUByte() - byte1.toUByte()).toInt()
if (compareResult != 0) {
break
}
publicSuffixByteIndex++
currentLabelByteIndex++
if (publicSuffixByteIndex == publicSuffixLength) {
break
}
if (labels[currentLabelIndex].size == currentLabelByteIndex) {
// We've exhausted our current label. Either there are more labels to compare, in which
// case we expect a dot as the next character. Otherwise, we've checked all our labels.
if (currentLabelIndex == labels.size - 1) {
break
} else {
currentLabelIndex++
currentLabelByteIndex = -1
expectDot = true
}
}
}
if (compareResult < 0) {
high = start - 1
} else if (compareResult > 0) {
low = start + end + 1
} else {
// We found a match, but are the lengths equal?
val publicSuffixBytesLeft = publicSuffixLength - publicSuffixByteIndex
var labelBytesLeft = labels[currentLabelIndex].size - currentLabelByteIndex
for (i in currentLabelIndex + 1 until labels.size) {
labelBytesLeft += labels[i].size
}
if (labelBytesLeft < publicSuffixBytesLeft) {
high = start - 1
} else if (labelBytesLeft > publicSuffixBytesLeft) {
low = start + end + 1
} else {
// Found a match.
match = String(this, start, publicSuffixLength, Charsets.UTF_8)
break
}
}
}
return match
}
/**
* Search for a '\n' that marks the start of a value. Don't go back past the start of the array.
*/
private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
var index = start
while (index > -1 && this[index] != '\n'.toByte()) {
index--
}
index++
return index
}
/**
* Search for a '\n' that marks the end of a value.
*/
private fun ByteArray.findEndOfLineFromIndex(start: Int): Int {
var end = 1
while (this[start + end] != '\n'.toByte()) {
end++
}
return end
}