001/* 002 * Copyright (C) 2026 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the License 010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 011 * or implied. See the License for the specific language governing permissions and limitations under 012 * the License. 013 */ 014 015package com.google.thirdparty.publicsuffix; 016 017import static java.lang.Math.min; 018 019import com.google.common.annotations.GwtCompatible; 020import com.google.common.collect.ImmutableList; 021import java.util.List; 022import org.jspecify.annotations.Nullable; 023 024/** 025 * A memory-efficient binary trie structure for public suffix lookups. The trie is stored as a 026 * series of 16-bit characters in a String. 027 * 028 * <p>The trie data is stored in a {@link CharSequence} where each node takes 3 characters: 029 * 030 * <ol> 031 * <li>The offset of the label in the {@code stringPool}. 032 * <li>The index of the first child node in the {@code trieData}. 033 * <li>A bitfield containing: 034 * <ul> 035 * <li>Bits 0-10: Number of children. 036 * <li>Bit 11: Whether this node represents an exclusion (!rule). 037 * <li>Bits 12-13: The type of wildcard match (*.rule) at this node (0=none, 1=REGISTRY, 038 * 2=PRIVATE). 039 * <li>Bits 14-15: The type of exact match (rule) at this node (0=none, 1=REGISTRY, 040 * 2=PRIVATE). 041 * </ul> 042 * </ol> 043 * 044 * <p>The {@code stringPool} stores the labels of the nodes as length-prefixed strings. 045 * 046 * <p><b>Capacity Limits and Scalability:</b> 047 * 048 * <ul> 049 * <li><b>Total nodes:</b> 65535 (16-bit index in the trie) 050 * <li><b>String pool size:</b> 65535 characters (16-bit offset in the String Pool) 051 * <li><b>Max children per node:</b> 2047 (11-bit count in the bitfield) 052 * </ul> 053 * 054 * <p>As of Jan 2026, the capacity usage is approximately: 055 * 056 * <ul> 057 * <li>Total nodes: ~16% (10k / 65k) 058 * <li>String pool size: ~79% (52k / 65k) 059 * <li>Max children per node: ~71% (1.5k / 2k) 060 * </ul> 061 * 062 * <p>If any of these limits are exceeded, the definitive solution is to increase the node size from 063 * 3 to 4 characters. This would allow: 064 * 065 * <ul> 066 * <li>A full 32-bit offset for the String Pool (using 2 characters). 067 * <li>A full 16-bit child count (using the freed bits from the 4th character). 068 * </ul> 069 */ 070@GwtCompatible 071public final class PublicSuffixTrie { 072 073 /** Each node in the trie takes up 3 characters. */ 074 static final int NODE_SIZE = 3; 075 076 static final int CHILDREN_MASK = 0x7FF; 077 static final int EXCLUSION_MASK = 0x1; 078 static final int TYPE_MASK = 0x3; 079 static final int EXCLUSION_SHIFT = 11; 080 static final int WILDCARD_SHIFT = 12; 081 static final int EXACT_SHIFT = 14; 082 083 final CharSequence trieData; 084 final CharSequence stringPool; 085 086 PublicSuffixTrie( 087 ImmutableList<CharSequence> trieDataChunks, 088 ImmutableList<CharSequence> stringPoolChunks, 089 int chunkShift) { 090 this.trieData = new ChunksCharSequence(trieDataChunks, chunkShift); 091 this.stringPool = new ChunksCharSequence(stringPoolChunks, chunkShift); 092 } 093 094 /** 095 * Performs a lookup for the public suffix of the given labels, considering all public suffix 096 * types. Returns the index of the leftmost label in the public suffix, or -1 if no match found. 097 * 098 * @param labels the domain labels, from left to right. 099 * @return the index of the leftmost label in the public suffix, or -1 if not found. 100 */ 101 public int findSuffixIndex(List<String> labels) { 102 return findSuffixIndex(labels, /* desiredType= */ null); 103 } 104 105 /** 106 * Performs a lookup for the public suffix of the given labels and returns the index of the 107 * leftmost label in the public suffix. Returns -1 if no match found. 108 * 109 * @param labels the domain labels, from left to right. 110 * @param desiredType the desired type of public suffix (e.g. REGISTRY or PRIVATE). If null, any 111 * {@link PublicSuffixType} will be considered a match. 112 * @return the index of the leftmost label in the public suffix, or -1 if not found. 113 */ 114 public int findSuffixIndex(List<String> labels, @Nullable PublicSuffixType desiredType) { 115 int partsSize = labels.size(); 116 int nodeIndex = 0; // Root node. 117 int bestResult = -1; 118 119 // Iterate over the labels in reverse order (from top level domain to the left). 120 for (int i = partsSize - 1; i >= 0; i--) { 121 // Decode the current node. 122 int firstChild = trieData.charAt(nodeIndex * NODE_SIZE + 1); 123 int numChildren = trieData.charAt(nodeIndex * NODE_SIZE + 2) & CHILDREN_MASK; 124 125 nodeIndex = findChild(firstChild, numChildren, labels.get(i)); 126 if (nodeIndex == -1) { 127 break; 128 } 129 130 // Check for matches at this node. Higher priority rules are checked last to overwrite. 131 int metadata = trieData.charAt(nodeIndex * NODE_SIZE + 2); 132 133 // Excluded match (!rule). If this bit is set, it means the current path is NOT a public 134 // suffix, even if a previous rule matched. For example, "!www.ck" would exclude "www.ck" 135 // from being a public suffix, leaving "ck" as the suffix. 136 if (isExcludedMatch(metadata)) { 137 bestResult = i + 1; 138 } 139 140 // Exact match. If the exact type bits are set, this node itself is a public suffix. 141 PublicSuffixType exactType = getExactMatchType(metadata); 142 if (matchesType(desiredType, exactType)) { 143 bestResult = i; 144 } 145 146 // Wildcard match (*.rule). If the wildcard type bits are set, any child of this node is a 147 // public suffix. For example, "*.ck" means "any.ck" is a public suffix. 148 PublicSuffixType wildcardType = getWildcardMatchType(metadata); 149 if (i > 0 && matchesType(desiredType, wildcardType)) { 150 bestResult = i - 1; 151 } 152 } 153 return bestResult; 154 } 155 156 private static boolean matchesType( 157 @Nullable PublicSuffixType desiredType, @Nullable PublicSuffixType actualType) { 158 return actualType != null && (desiredType == null || desiredType.equals(actualType)); 159 } 160 161 private static boolean isExcludedMatch(int metadata) { 162 return ((metadata >> EXCLUSION_SHIFT) & EXCLUSION_MASK) != 0; 163 } 164 165 private @Nullable PublicSuffixType getExactMatchType(int metadata) { 166 return getType((metadata >> EXACT_SHIFT) & TYPE_MASK); 167 } 168 169 private @Nullable PublicSuffixType getWildcardMatchType(int metadata) { 170 return getType((metadata >> WILDCARD_SHIFT) & TYPE_MASK); 171 } 172 173 private @Nullable PublicSuffixType getType(int typeBits) { 174 if (typeBits == 1) { 175 return PublicSuffixType.REGISTRY; 176 } else if (typeBits == 2) { 177 return PublicSuffixType.PRIVATE; 178 } 179 return null; 180 } 181 182 /** 183 * Performs a search over the children of a node to find the child with the given label. 184 * 185 * @return the index of the child node if found, or -1 otherwise. 186 */ 187 private int findChild(int firstChild, int numChildren, String label) { 188 int low = firstChild; 189 int high = firstChild + numChildren - 1; 190 191 while (low <= high) { 192 int mid = (low + high) >>> 1; 193 int labelOffset = trieData.charAt(mid * NODE_SIZE); 194 int cmp = compareLabel(label, labelOffset); 195 196 if (cmp < 0) { 197 high = mid - 1; 198 } else if (cmp > 0) { 199 low = mid + 1; 200 } else { 201 return mid; 202 } 203 } 204 return -1; 205 } 206 207 /** 208 * Returns a negative integer, zero, or a positive integer as the specified label is 209 * lexicographically less than, equal to, or greater than the label in the string pool. 210 */ 211 private int compareLabel(String label, int offset) { 212 int labelLen = label.length(); 213 int nodeLabelLen = stringPool.charAt(offset); 214 int minLen = min(nodeLabelLen, labelLen); 215 for (int i = 0; i < minLen; i++) { 216 char c1 = label.charAt(i); 217 char c2 = stringPool.charAt(offset + 1 + i); 218 if (c1 != c2) { 219 return c1 - c2; 220 } 221 } 222 return labelLen - nodeLabelLen; 223 } 224 225 /** 226 * A CharSequence that is composed of multiple chunks. This is used to work around the 64k size 227 * limit for string literals in Java. 228 */ 229 private static class ChunksCharSequence implements CharSequence { 230 private final ImmutableList<CharSequence> chunks; 231 private final int length; 232 private final int chunkShift; 233 private final int chunkMask; 234 235 ChunksCharSequence(ImmutableList<CharSequence> chunks, int chunkShift) { 236 this.chunks = chunks; 237 this.chunkShift = chunkShift; 238 this.chunkMask = (1 << chunkShift) - 1; 239 int total = 0; 240 for (CharSequence element : chunks) { 241 total += element.length(); 242 } 243 this.length = total; 244 } 245 246 @Override 247 public int length() { 248 return length; 249 } 250 251 @Override 252 public char charAt(int index) { 253 return chunks.get(index >> chunkShift).charAt(index & chunkMask); 254 } 255 256 @Override 257 public CharSequence subSequence(int start, int end) { 258 throw new UnsupportedOperationException(); 259 } 260 261 @Override 262 public String toString() { 263 StringBuilder sb = new StringBuilder(length); 264 for (CharSequence chunk : chunks) { 265 sb.append(chunk); 266 } 267 return sb.toString(); 268 } 269 } 270}