aboutsummaryrefslogtreecommitdiffstats
path: root/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
blob: 6f503b76ffd6d565a9835769400b93cf87aa55de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/*
 * 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<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] != 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<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 ("$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<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 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<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] = 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 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<String>()
    const val EXCEPTION_MARKER = '!'
  }
}

internal sealed class PublicSuffixOffset {
  data class Offset(val value: Int) : PublicSuffixOffset()

  object PublicSuffix : PublicSuffixOffset()

  object PrevailingRule : PublicSuffixOffset()
}