| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- // NOTE: kept (intentionally) near-identical to packages/tldts-icann/src/suffix-trie.ts.
- // They are separate copies rather than a shared helper because the lookup is
- // only fast when the typed arrays are module-scope monomorphic globals —
- // closing over them (a shared factory) measured ~20% slower. The ICANN build
- // also specializes (constant mask, no isIcann/isPrivate). Keep the two in sync.
- import {
- fastPathLookup,
- IPublicSuffix,
- ISuffixLookupOptions,
- } from 'tldts-core';
- import {
- edgeChild,
- edgeLength,
- edgeStart,
- exceptionsRoot,
- labelText,
- nodeFlags,
- rulesRoot,
- } from './data/trie';
- const enum RULE_TYPE {
- ICANN = 1,
- PRIVATE = 2,
- }
- // `edgeOffset` (where each label starts in `labelText`), `edgeHash` (djb2 of
- // each label) and `wildcardEdge` (each node's '*' edge, or -1) are derived once
- // at load instead of being shipped: the bundle then carries only the
- // compressible `labelText` + structure, while the lookup binary-searches
- // integer hashes. The cost is a single ~1ms pass at first import — cheaper than
- // the object trie it replaces. Kept at module scope (not captured in a closure)
- // so V8 treats the typed arrays as fast monomorphic globals.
- const numberOfNodes = nodeFlags.length;
- const numberOfEdges = edgeLength.length;
- const edgeOffset = new Uint32Array(numberOfEdges);
- const edgeHash = new Uint32Array(numberOfEdges);
- const wildcardEdge = new Int32Array(numberOfNodes).fill(-1);
- for (let node = 0, offset = 0; node < numberOfNodes; node += 1) {
- for (let edge = edgeStart[node]!; edge < edgeStart[node + 1]!; edge += 1) {
- edgeOffset[edge] = offset;
- const end = offset + edgeLength[edge]!;
- let hash = 5381;
- for (let i = end - 1; i >= offset; i -= 1) {
- hash = (hash * 33) ^ labelText.charCodeAt(i);
- }
- edgeHash[edge] = hash >>> 0;
- if (
- edgeLength[edge] === 1 &&
- labelText.charCodeAt(offset) === 42 /* '*' */
- ) {
- wildcardEdge[node] = edge;
- }
- offset = end;
- }
- }
- // Result of the last `walk`, kept in module scope to avoid allocating a match
- // object. Safe because lookups are synchronous and read right after `walk`.
- let matchNode = -1;
- let matchStart = 0;
- let matchEnd = 0;
- /**
- * True if edge `edge`'s label equals `hostname[start, start + length)`.
- */
- function labelEquals(
- edge: number,
- hostname: string,
- start: number,
- length: number,
- ): boolean {
- if (edgeLength[edge] !== length) {
- return false;
- }
- const offset = edgeOffset[edge]!;
- for (let i = 0; i < length; i += 1) {
- if (labelText.charCodeAt(offset + i) !== hostname.charCodeAt(start + i)) {
- return false;
- }
- }
- return true;
- }
- /**
- * Find the child edge of `node` whose label is `hostname[start, start + length)`.
- * Edges are sorted by hash, so binary-search the hash then verify the label
- * (scanning the rare run of equal hashes). Returns the edge index or -1.
- */
- function findEdge(
- node: number,
- hash: number,
- hostname: string,
- start: number,
- length: number,
- ): number {
- let lo = edgeStart[node]!;
- let hi = edgeStart[node + 1]!;
- while (lo < hi) {
- const mid = (lo + hi) >>> 1;
- const value = edgeHash[mid]!;
- if (value < hash) {
- lo = mid + 1;
- } else if (value > hash) {
- hi = mid;
- } else {
- for (let e = mid; e >= lo && edgeHash[e] === hash; e -= 1) {
- if (labelEquals(e, hostname, start, length)) return e;
- }
- for (let e = mid + 1; e < hi && edgeHash[e] === hash; e += 1) {
- if (labelEquals(e, hostname, start, length)) return e;
- }
- return -1;
- }
- }
- return -1;
- }
- /**
- * Walk `hostname`'s labels right-to-left from `root`, recording the deepest
- * node whose flag passes `allowedMask` (with the label boundaries of that match
- * in `matchStart`/`matchEnd`). Returns whether any match was found.
- */
- function walk(hostname: string, root: number, allowedMask: number): boolean {
- let node = root;
- let end = hostname.length;
- let hash = 5381;
- matchNode = -1;
- for (let i = hostname.length - 1; i >= 0; i -= 1) {
- const code = hostname.charCodeAt(i);
- if (code === 46 /* '.' */) {
- const start = i + 1;
- let edge = findEdge(node, hash >>> 0, hostname, start, end - start);
- if (edge === -1) {
- edge = wildcardEdge[node]!;
- }
- if (edge === -1) {
- return matchNode !== -1;
- }
- node = edgeChild[edge]!;
- if ((nodeFlags[node]! & allowedMask) !== 0) {
- matchNode = node;
- matchStart = start;
- matchEnd = end;
- }
- end = i;
- hash = 5381;
- } else {
- hash = (hash * 33) ^ code;
- }
- }
- // Left-most label: hostname[0, end). Same find/descend/record as the loop —
- // duplicated rather than folded into the loop (via `i >= -1`) because that
- // extra per-character branch measured slightly slower on the hot path.
- let edge = findEdge(node, hash >>> 0, hostname, 0, end);
- if (edge === -1) {
- edge = wildcardEdge[node]!;
- }
- if (edge !== -1) {
- node = edgeChild[edge]!;
- if ((nodeFlags[node]! & allowedMask) !== 0) {
- matchNode = node;
- matchStart = 0;
- matchEnd = end;
- }
- }
- return matchNode !== -1;
- }
- /**
- * Check if `hostname` has a valid public suffix in the trie.
- */
- export default function suffixLookup(
- hostname: string,
- options: ISuffixLookupOptions,
- out: IPublicSuffix,
- ): void {
- if (fastPathLookup(hostname, options, out)) {
- return;
- }
- const allowedMask =
- (options.allowPrivateDomains ? RULE_TYPE.PRIVATE : 0) |
- (options.allowIcannDomains ? RULE_TYPE.ICANN : 0);
- // Exceptions have priority and strip their own left-most label (e.g. the
- // rule '!www.ck' makes the suffix of 'www.ck' be 'ck').
- if (walk(hostname, exceptionsRoot, allowedMask)) {
- out.isIcann = (nodeFlags[matchNode]! & RULE_TYPE.ICANN) !== 0;
- out.isPrivate = (nodeFlags[matchNode]! & RULE_TYPE.PRIVATE) !== 0;
- out.publicSuffix = hostname.slice(matchEnd + 1);
- return;
- }
- if (walk(hostname, rulesRoot, allowedMask)) {
- out.isIcann = (nodeFlags[matchNode]! & RULE_TYPE.ICANN) !== 0;
- out.isPrivate = (nodeFlags[matchNode]! & RULE_TYPE.PRIVATE) !== 0;
- out.publicSuffix = hostname.slice(matchStart);
- return;
- }
- // No match: the prevailing '*' rule makes the right-most label the suffix.
- out.isIcann = false;
- out.isPrivate = false;
- const lastDot = hostname.lastIndexOf('.');
- out.publicSuffix = lastDot === -1 ? hostname : hostname.slice(lastDot + 1);
- }
|