/* * SPDX-License-Identifier: (LGPL-3.0-only WITH LGPL-3.0-linking-exception) OR MPL-2.0 */ /* 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 java.net.IDN import mozilla.components.lib.publicsuffixlist.ext.binarySearch /** 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, labelIndex: Int): String? { return rules.binarySearch(labels, labelIndex) } private fun binarySearchExceptions(labels: List, 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] != EXCEPTION_MARKER) { // The domain is a public suffix. return if (rule == PREVAILING_RULE) { PublicSuffixOffset.PrevailingRule } else { PublicSuffixOffset.PublicSuffix } } return if (rule[0][0] == 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): List { // 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 ("$EXCEPTION_MARKER$exceptionMatch").split('.') } if (exactMatch == null && wildcardMatch == null) { return PREVAILING_RULE } val exactRuleLabels = exactMatch?.split('.') ?: EMPTY_RULE val wildcardRuleLabels = wildcardMatch?.split('.') ?: EMPTY_RULE return if (exactRuleLabels.size > wildcardRuleLabels.size) { exactRuleLabels } else { wildcardRuleLabels } } /** Returns an exact match or null. */ private fun findExactMatch(labels: List): 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 labels.indices) { val rule = binarySearchRules(labels, i) if (rule != null) { return rule } } return null } /** Returns a wildcard match or null. */ private fun findWildcardMatch(labels: List): 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] = WILDCARD_LABEL val rule = binarySearchRules(labelsWithWildcard, labelIndex) if (rule != null) { return rule } } } return null } private fun findExceptionMatch(labels: List, 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 labels.indices) { val rule = binarySearchExceptions(labels, labelIndex) if (rule != null) { return rule } } return null } companion object { val WILDCARD_LABEL = byteArrayOf('*'.code.toByte()) val PREVAILING_RULE = listOf("*") val EMPTY_RULE = listOf() const val EXCEPTION_MARKER = '!' } } internal sealed class PublicSuffixOffset { data class Offset(val value: Int) : PublicSuffixOffset() object PublicSuffix : PublicSuffixOffset() object PrevailingRule : PublicSuffixOffset() }