tldts-utils 7.0.32 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,208 +1,106 @@
1
1
  /**
2
- * This builder implements a fast and compact DAWG data-structure (a.k.a.:
3
- * direct acyclic word graph, with suffix and prefix compression of the
4
- * top-level domains) using JavaScript Objects. It is a good trade-off between
5
- * correctness, speed and space. This is much faster then using a linear scan
6
- * over all the rules, but slightly slower than the probabilistic
7
- * data-structure implemented in `bin/builders/hashes.js`.
2
+ * Builds a flat, allocation-free public-suffix trie.
8
3
  *
9
- * Here is a simple example:
4
+ * The rules are inserted into an in-memory trie, identical sub-trees are shared
5
+ * (DAWG compression), and the result is emitted as a handful of flat typed
6
+ * arrays plus one `text` string holding every edge label:
10
7
  *
11
- * co.uk
12
- * gov.uk
13
- * fr
8
+ * nodeFlags[node] -> 0 (none) | 1 (ICANN) | 2 (PRIVATE)
9
+ * edgeStart[node] -> first edge index of `node` (edges of a node are a
10
+ * contiguous, hash-sorted slice [edgeStart[node], edgeStart[node+1]))
11
+ * edgeLength[edge] -> label length
12
+ * edgeChild[edge] -> destination node
13
+ * labelText -> all edge labels concatenated in edge order
14
14
  *
15
- * <root>
16
- * |_ uk
17
- * |_ co *
18
- * |_ gov *
19
- * |_ fr *
15
+ * The per-edge label offsets (prefix-sum of `edgeLength`) and label hashes
16
+ * (djb2 over `labelText`) are NOT shipped — they are derived once at load in
17
+ * `suffix-trie.ts`. This keeps the bundle small (only compressible text +
18
+ * structure) while the lookup binary-searches integer hashes at runtime.
20
19
  *
21
- * Finding a match is then a matter of iterating on labels of a hostname
22
- * backward and following the branches of the Trie. Whenever we find a terminal
23
- * node (indicated with a '*' above), we consider this a match.
20
+ * Lookup walks a hostname's labels right-to-left following edges; a flagged
21
+ * node is a public-suffix match. See `suffix-trie.ts`.
24
22
  */
25
23
 
26
24
  import parse from '../parser';
27
25
 
28
26
  interface ITrie {
29
- $: 0 | 1 | 2;
30
- succ: Record<string, ITrie>;
31
- }
32
-
33
- type CompactTrie = [0 | 1 | 2, Record<string, CompactTrie>];
34
-
35
- function convertToCompactTrie(trie: ITrie): CompactTrie {
36
- return [
37
- trie.$,
38
- Object.fromEntries(
39
- Object.entries(trie.succ).map(([label, succ]) => [
40
- label,
41
- typeof succ === 'string' ? succ : convertToCompactTrie(succ),
42
- ]),
43
- ),
44
- ];
27
+ flag: 0 | 1 | 2;
28
+ children: Record<string, ITrie>;
45
29
  }
46
30
 
47
31
  /**
48
- * Insert a public suffix rule in the `trie`.
32
+ * Insert a public suffix rule (its labels, already reversed) into the `trie`.
49
33
  */
50
34
  function insertInTrie(
51
- {
52
- parts,
53
- isIcann,
54
- }: {
55
- parts: string[];
56
- isIcann: boolean;
57
- },
35
+ { parts, isIcann }: { parts: string[]; isIcann: boolean },
58
36
  trie: ITrie,
59
- ): ITrie {
60
- let node: ITrie = trie;
61
-
37
+ ): void {
38
+ let node = trie;
62
39
  for (const part of parts) {
63
- let nextNode = node.succ[part];
64
- if (nextNode === undefined) {
65
- nextNode = { $: 0, succ: {} };
66
- node.succ[part] = nextNode;
40
+ let next = node.children[part];
41
+ if (next === undefined) {
42
+ next = { flag: 0, children: {} };
43
+ node.children[part] = next;
67
44
  }
68
-
69
- node = nextNode;
45
+ node = next;
70
46
  }
71
-
72
- node.$ = isIcann ? 1 : 2;
73
-
74
- return trie;
47
+ node.flag = isIcann ? 1 : 2;
75
48
  }
76
49
 
77
- let nextId = 0;
78
-
79
50
  /**
80
- * Compress the given `trie` into a DAWG (by compressing common suffixes as
81
- * well as prefixes). This form is very efficient but cannot be dumped as a
82
- * simple JSON into the file. The solution is to create intermediary variables
83
- * for each shared sub-tree, which are then used in the main DAWG.
51
+ * djb2 hash over `label`, computed backward. MUST stay identical to the
52
+ * load-time recomputation in `suffix-trie.ts` so edges remain hash-sorted.
84
53
  */
85
- function compressToDAWG(trie: ITrie, name: string): string {
86
- const nodesByHash = new Map<string, ITrie[]>();
87
- const replaceNodes = new Map<ITrie, (newNode: ITrie) => void>();
88
-
89
- (function groupNodesByHash(node: ITrie): string {
90
- // Get a sorted list of next labels from this node
91
- const nexts = Object.entries(node.succ)
92
- .filter(([n]) => n !== '$')
93
- .sort();
94
-
95
- // Create a setter which can be used later to replace the value of a node.
96
- // Each node is associated with a lambda function accepting one argument:
97
- // the new value of the node. Because the context of the node (parent and
98
- // label) is captured in the closure, it allows to just call it later, in a
99
- // different context, to change the value of a given node in-place.
100
- nexts.forEach(([n, succ]) => {
101
- replaceNodes.set(succ, (newNode: ITrie) => {
102
- node.succ[n] = newNode;
103
- });
104
- });
105
-
106
- // Compute a unique hash for this node recursively.
107
- const hash = `(${node.$},${[
108
- ...nexts.map(([c, succ]) => `${c},${groupNodesByHash(succ)}`),
109
- ].join('|')})`;
110
-
111
- // Keep track of all node's hashes
112
- let nodes: undefined | ITrie[] = nodesByHash.get(hash);
113
- if (nodes === undefined) {
114
- nodes = [];
115
- nodesByHash.set(hash, nodes);
116
- }
117
- nodes.push(node);
118
-
119
- return hash;
120
- })(trie);
121
-
122
- // Given `nodesByHash`, which associates a list of nodes to each hash
123
- // encountered in the previous step, we will detect all the sub-trees being
124
- // seen more than once and store them in a variable to allow sharing with
125
- // different parts in the DAWG.
126
- const variables: string[] = [];
127
- nodesByHash.forEach((nodes) => {
128
- if (nodes.length > 1) {
129
- // Dump one of the nodes (they are all the same so it does not matter which one)
130
- variables.push(
131
- `_${nextId}: ITrie = ${JSON.stringify(
132
- convertToCompactTrie(nodes[0]!),
133
- )}`,
134
- );
135
-
136
- // Replace all the other ones by the name of this new variable
137
- nodes.forEach((node) => {
138
- // @ts-expect-error replaceNodes callback expects ITrie, we pass string ref
139
- replaceNodes.get(node)?.(`_${nextId}`);
140
- });
141
-
142
- nextId += 1;
143
- }
144
- });
145
-
146
- const output = [];
147
-
148
- // Create TypeScript source code to declare all of these variables. Because
149
- // the value of nodes has been set to a value of the form '"_id"', we need to
150
- // replace this on the fly to '_id'. We use a RegExp for this.
151
- if (variables.length !== 0) {
152
- let variablesSourceCode = `const ${variables.join(',')};`;
153
- for (let i = 0; i < nextId; i += 1) {
154
- variablesSourceCode = variablesSourceCode.replace(
155
- new RegExp(`"_${i}"`, 'g'),
156
- `_${i}`,
157
- );
158
- }
159
- output.push(variablesSourceCode);
54
+ function labelHash(label: string): number {
55
+ let hash = 5381;
56
+ for (let i = label.length - 1; i >= 0; i -= 1) {
57
+ hash = (hash * 33) ^ label.charCodeAt(i);
160
58
  }
161
-
162
- // Dump the root of the DAWG as well, and perform the same replacement of _id
163
- // parts than for the variables above.
164
- let serializedTrie = JSON.stringify(convertToCompactTrie(trie));
165
- for (let i = 0; i < nextId; i += 1) {
166
- serializedTrie = serializedTrie.replace(
167
- new RegExp(`"_${i}"`, 'g'),
168
- `_${i}`,
169
- );
170
- }
171
- output.push(`const ${name}: ITrie = ${serializedTrie};`);
172
-
173
- return output.join('\n');
59
+ return hash >>> 0;
174
60
  }
175
61
 
176
- function convertToTypeScriptSource(
177
- rules: ITrie,
178
- exceptions: ITrie,
179
- { includePrivate }: { includePrivate: boolean },
180
- ): string {
181
- return `
182
- export type ITrie = [${
183
- includePrivate ? '0 | 1 | 2' : '0 | 1'
184
- }, { [label: string]: ITrie}];
62
+ interface FlatNode {
63
+ flag: 0 | 1 | 2;
64
+ edges: { label: string; child: number }[];
65
+ }
185
66
 
186
- export const exceptions: ITrie = (function() {
187
- ${compressToDAWG(exceptions, 'exceptions')}
188
- return exceptions;
189
- })();
67
+ /**
68
+ * Flatten the given tries into a shared pool of nodes (identical sub-trees get
69
+ * the same id => DAWG compression). Returns the node pool and each root's id.
70
+ */
71
+ function flatten(roots: ITrie[]): { nodes: FlatNode[]; rootIds: number[] } {
72
+ const idByKey = new Map<string, number>();
73
+ const nodes: FlatNode[] = [];
74
+
75
+ const visit = (node: ITrie): number => {
76
+ const edges = Object.keys(node.children)
77
+ .sort()
78
+ .map((label) => ({ label, child: visit(node.children[label]!) }));
79
+
80
+ // Identity key for DAWG dedup: same flag + same (label -> child) set. Built
81
+ // from the alpha-sorted edges so it is canonical; the stored copy is then
82
+ // re-sorted by hash because the lookup binary-searches edge hashes.
83
+ const key = `${node.flag}|${edges.map((e) => `${e.label}>${e.child}`).join(',')}`;
84
+ let id = idByKey.get(key);
85
+ if (id === undefined) {
86
+ edges.sort((a, b) => labelHash(a.label) - labelHash(b.label));
87
+ id = nodes.length;
88
+ nodes.push({ flag: node.flag, edges });
89
+ idByKey.set(key, id);
90
+ }
91
+ return id;
92
+ };
190
93
 
191
- export const rules: ITrie = (function() {
192
- ${compressToDAWG(rules, 'rules')}
193
- return rules;
194
- })();
195
- `;
94
+ return { nodes, rootIds: roots.map(visit) };
196
95
  }
197
96
 
198
97
  export default (
199
98
  body: string,
200
99
  { includePrivate }: { includePrivate: boolean },
201
- ) => {
202
- const exceptions: ITrie = { $: 0, succ: {} };
203
- const rules: ITrie = { $: 0, succ: {} };
100
+ ): string => {
101
+ const rules: ITrie = { flag: 0, children: {} };
102
+ const exceptions: ITrie = { flag: 0, children: {} };
204
103
 
205
- // Iterate on public suffix rules
206
104
  parse(body, ({ rule, isIcann, isException }) => {
207
105
  if (isIcann || includePrivate) {
208
106
  insertInTrie(
@@ -212,5 +110,60 @@ export default (
212
110
  }
213
111
  });
214
112
 
215
- return convertToTypeScriptSource(rules, exceptions, { includePrivate });
113
+ const { nodes, rootIds } = flatten([rules, exceptions]);
114
+
115
+ const nodeFlags: number[] = [];
116
+ const edgeStart: number[] = [0];
117
+ const edgeLength: number[] = [];
118
+ const edgeChild: number[] = [];
119
+ let labelText = '';
120
+ for (const node of nodes) {
121
+ nodeFlags.push(node.flag);
122
+ for (const edge of node.edges) {
123
+ edgeLength.push(edge.label.length);
124
+ edgeChild.push(edge.child);
125
+ labelText += edge.label;
126
+ }
127
+ edgeStart.push(edgeLength.length);
128
+ }
129
+
130
+ // The serialized arrays ship as typed arrays (see the emitted code below):
131
+ // edgeChild/edgeStart as Uint16Array, edgeLength as Uint8Array. If the PSL
132
+ // ever outgrows those ranges the values would silently wrap and corrupt every
133
+ // lookup, so fail the build loudly instead. Headroom is large today (~600
134
+ // nodes / ~10k edges / max label 31 vs the 65535/255 ceilings). If this trips,
135
+ // widen the array(s) here AND in the two suffix-trie.ts loaders.
136
+ let maxEdgeLength = 0;
137
+ for (const length of edgeLength) {
138
+ if (length > maxEdgeLength) {
139
+ maxEdgeLength = length;
140
+ }
141
+ }
142
+ if (nodes.length > 65536) {
143
+ // edgeChild stores node ids in [0, nodes.length); max id must fit Uint16.
144
+ throw new Error(
145
+ `trie builder: ${nodes.length} nodes exceeds the Uint16 edgeChild range (max 65536). Widen edgeChild + edgeStart to Uint32Array here and in suffix-trie.ts.`,
146
+ );
147
+ }
148
+ if (edgeLength.length > 65535) {
149
+ // edgeStart stores edge indices up to the edge count; must fit Uint16.
150
+ throw new Error(
151
+ `trie builder: ${edgeLength.length} edges exceeds the Uint16 edgeStart range (max 65535). Widen edgeStart + edgeChild to Uint32Array here and in suffix-trie.ts.`,
152
+ );
153
+ }
154
+ if (maxEdgeLength > 255) {
155
+ throw new Error(
156
+ `trie builder: label length ${maxEdgeLength} exceeds the Uint8 edgeLength range (max 255). Widen edgeLength to Uint16Array here and in suffix-trie.ts.`,
157
+ );
158
+ }
159
+
160
+ return `// Auto-generated flat public-suffix trie. Do not edit.
161
+ export const nodeFlags = /*#__PURE__*/ new Uint8Array([${nodeFlags.join(',')}]);
162
+ export const edgeStart = /*#__PURE__*/ new Uint16Array([${edgeStart.join(',')}]);
163
+ export const edgeLength = /*#__PURE__*/ new Uint8Array([${edgeLength.join(',')}]);
164
+ export const edgeChild = /*#__PURE__*/ new Uint16Array([${edgeChild.join(',')}]);
165
+ export const labelText = ${JSON.stringify(labelText)};
166
+ export const rulesRoot = ${rootIds[0]};
167
+ export const exceptionsRoot = ${rootIds[1]};
168
+ `;
216
169
  };