suffix-trie.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. // NOTE: kept (intentionally) near-identical to packages/tldts-icann/src/suffix-trie.ts.
  2. // They are separate copies rather than a shared helper because the lookup is
  3. // only fast when the typed arrays are module-scope monomorphic globals —
  4. // closing over them (a shared factory) measured ~20% slower. The ICANN build
  5. // also specializes (constant mask, no isIcann/isPrivate). Keep the two in sync.
  6. import {
  7. fastPathLookup,
  8. IPublicSuffix,
  9. ISuffixLookupOptions,
  10. } from 'tldts-core';
  11. import {
  12. edgeChild,
  13. edgeLength,
  14. edgeStart,
  15. exceptionsRoot,
  16. labelText,
  17. nodeFlags,
  18. rulesRoot,
  19. } from './data/trie';
  20. const enum RULE_TYPE {
  21. ICANN = 1,
  22. PRIVATE = 2,
  23. }
  24. // `edgeOffset` (where each label starts in `labelText`), `edgeHash` (djb2 of
  25. // each label) and `wildcardEdge` (each node's '*' edge, or -1) are derived once
  26. // at load instead of being shipped: the bundle then carries only the
  27. // compressible `labelText` + structure, while the lookup binary-searches
  28. // integer hashes. The cost is a single ~1ms pass at first import — cheaper than
  29. // the object trie it replaces. Kept at module scope (not captured in a closure)
  30. // so V8 treats the typed arrays as fast monomorphic globals.
  31. const numberOfNodes = nodeFlags.length;
  32. const numberOfEdges = edgeLength.length;
  33. const edgeOffset = new Uint32Array(numberOfEdges);
  34. const edgeHash = new Uint32Array(numberOfEdges);
  35. const wildcardEdge = new Int32Array(numberOfNodes).fill(-1);
  36. for (let node = 0, offset = 0; node < numberOfNodes; node += 1) {
  37. for (let edge = edgeStart[node]!; edge < edgeStart[node + 1]!; edge += 1) {
  38. edgeOffset[edge] = offset;
  39. const end = offset + edgeLength[edge]!;
  40. let hash = 5381;
  41. for (let i = end - 1; i >= offset; i -= 1) {
  42. hash = (hash * 33) ^ labelText.charCodeAt(i);
  43. }
  44. edgeHash[edge] = hash >>> 0;
  45. if (
  46. edgeLength[edge] === 1 &&
  47. labelText.charCodeAt(offset) === 42 /* '*' */
  48. ) {
  49. wildcardEdge[node] = edge;
  50. }
  51. offset = end;
  52. }
  53. }
  54. // Result of the last `walk`, kept in module scope to avoid allocating a match
  55. // object. Safe because lookups are synchronous and read right after `walk`.
  56. let matchNode = -1;
  57. let matchStart = 0;
  58. let matchEnd = 0;
  59. /**
  60. * True if edge `edge`'s label equals `hostname[start, start + length)`.
  61. */
  62. function labelEquals(
  63. edge: number,
  64. hostname: string,
  65. start: number,
  66. length: number,
  67. ): boolean {
  68. if (edgeLength[edge] !== length) {
  69. return false;
  70. }
  71. const offset = edgeOffset[edge]!;
  72. for (let i = 0; i < length; i += 1) {
  73. if (labelText.charCodeAt(offset + i) !== hostname.charCodeAt(start + i)) {
  74. return false;
  75. }
  76. }
  77. return true;
  78. }
  79. /**
  80. * Find the child edge of `node` whose label is `hostname[start, start + length)`.
  81. * Edges are sorted by hash, so binary-search the hash then verify the label
  82. * (scanning the rare run of equal hashes). Returns the edge index or -1.
  83. */
  84. function findEdge(
  85. node: number,
  86. hash: number,
  87. hostname: string,
  88. start: number,
  89. length: number,
  90. ): number {
  91. let lo = edgeStart[node]!;
  92. let hi = edgeStart[node + 1]!;
  93. while (lo < hi) {
  94. const mid = (lo + hi) >>> 1;
  95. const value = edgeHash[mid]!;
  96. if (value < hash) {
  97. lo = mid + 1;
  98. } else if (value > hash) {
  99. hi = mid;
  100. } else {
  101. for (let e = mid; e >= lo && edgeHash[e] === hash; e -= 1) {
  102. if (labelEquals(e, hostname, start, length)) return e;
  103. }
  104. for (let e = mid + 1; e < hi && edgeHash[e] === hash; e += 1) {
  105. if (labelEquals(e, hostname, start, length)) return e;
  106. }
  107. return -1;
  108. }
  109. }
  110. return -1;
  111. }
  112. /**
  113. * Walk `hostname`'s labels right-to-left from `root`, recording the deepest
  114. * node whose flag passes `allowedMask` (with the label boundaries of that match
  115. * in `matchStart`/`matchEnd`). Returns whether any match was found.
  116. */
  117. function walk(hostname: string, root: number, allowedMask: number): boolean {
  118. let node = root;
  119. let end = hostname.length;
  120. let hash = 5381;
  121. matchNode = -1;
  122. for (let i = hostname.length - 1; i >= 0; i -= 1) {
  123. const code = hostname.charCodeAt(i);
  124. if (code === 46 /* '.' */) {
  125. const start = i + 1;
  126. let edge = findEdge(node, hash >>> 0, hostname, start, end - start);
  127. if (edge === -1) {
  128. edge = wildcardEdge[node]!;
  129. }
  130. if (edge === -1) {
  131. return matchNode !== -1;
  132. }
  133. node = edgeChild[edge]!;
  134. if ((nodeFlags[node]! & allowedMask) !== 0) {
  135. matchNode = node;
  136. matchStart = start;
  137. matchEnd = end;
  138. }
  139. end = i;
  140. hash = 5381;
  141. } else {
  142. hash = (hash * 33) ^ code;
  143. }
  144. }
  145. // Left-most label: hostname[0, end). Same find/descend/record as the loop —
  146. // duplicated rather than folded into the loop (via `i >= -1`) because that
  147. // extra per-character branch measured slightly slower on the hot path.
  148. let edge = findEdge(node, hash >>> 0, hostname, 0, end);
  149. if (edge === -1) {
  150. edge = wildcardEdge[node]!;
  151. }
  152. if (edge !== -1) {
  153. node = edgeChild[edge]!;
  154. if ((nodeFlags[node]! & allowedMask) !== 0) {
  155. matchNode = node;
  156. matchStart = 0;
  157. matchEnd = end;
  158. }
  159. }
  160. return matchNode !== -1;
  161. }
  162. /**
  163. * Check if `hostname` has a valid public suffix in the trie.
  164. */
  165. export default function suffixLookup(
  166. hostname: string,
  167. options: ISuffixLookupOptions,
  168. out: IPublicSuffix,
  169. ): void {
  170. if (fastPathLookup(hostname, options, out)) {
  171. return;
  172. }
  173. const allowedMask =
  174. (options.allowPrivateDomains ? RULE_TYPE.PRIVATE : 0) |
  175. (options.allowIcannDomains ? RULE_TYPE.ICANN : 0);
  176. // Exceptions have priority and strip their own left-most label (e.g. the
  177. // rule '!www.ck' makes the suffix of 'www.ck' be 'ck').
  178. if (walk(hostname, exceptionsRoot, allowedMask)) {
  179. out.isIcann = (nodeFlags[matchNode]! & RULE_TYPE.ICANN) !== 0;
  180. out.isPrivate = (nodeFlags[matchNode]! & RULE_TYPE.PRIVATE) !== 0;
  181. out.publicSuffix = hostname.slice(matchEnd + 1);
  182. return;
  183. }
  184. if (walk(hostname, rulesRoot, allowedMask)) {
  185. out.isIcann = (nodeFlags[matchNode]! & RULE_TYPE.ICANN) !== 0;
  186. out.isPrivate = (nodeFlags[matchNode]! & RULE_TYPE.PRIVATE) !== 0;
  187. out.publicSuffix = hostname.slice(matchStart);
  188. return;
  189. }
  190. // No match: the prevailing '*' rule makes the right-most label the suffix.
  191. out.isIcann = false;
  192. out.isPrivate = false;
  193. const lastDot = hostname.lastIndexOf('.');
  194. out.publicSuffix = lastDot === -1 ? hostname : hostname.slice(lastDot + 1);
  195. }